Checkpoint commit.

- Add the majority of the list behavioral knobs.
 - Add some quick helpers that can make list changes
   safely (write, unlink, touch)
 - Fix tests.
This commit is contained in:
Mahlon E. Smith 2017-02-06 11:54:16 -08:00
parent 5a524b55bf
commit 7e2a6fe771
32 changed files with 968 additions and 627 deletions

4
.hgignore Normal file
View file

@ -0,0 +1,4 @@
Session.vim
pkg/*
docs/*

View file

@ -6,23 +6,26 @@ code
## Authors
* Mahlon E. Smith <mahlon@martini.nu>
* Michael Granger <ged@faeriemud.org>
* Jeremiah Jordan <jjordan@laika.com>
* Mahlon E. Smith <mahlon@martini.nu>
## Description
This is a ruby interface for interacting with ezmlm-idx, an email list
manager for use with the Qmail MTA. (The -idx provides an extended
feature set over the initial ezmlm environment), and messages therein.
manager for use with the Qmail MTA, and the messages contained therein.
(The -idx provides an extended feature set over the original ezmlm
environment.)
http://untroubled.org/ezmlm/
This was tested against ezmlm-idx 7.2.2.
## Prerequisites
* Ruby 2.2 or better
* Ruby 2.1 or better
## Installation
@ -30,8 +33,16 @@ http://untroubled.org/ezmlm/
$ gem install ezmlm
## TODO
- Text file editing (trailers, etc.)
- Header / mime list accessors
## Limitations
This library doesn't create new lists from scratch. Use ezmlm-make.
This library is designed to only work with lists stored on disk (the
default), not the SQL backends.
@ -40,8 +51,11 @@ If things aren't adding up, make sure this library is running on a
machine with a matching address space as the list itself. (Running this
on a 64bit machine to talk to 32bit listserv isn't going to play well.)
The option set offered with ezmlm-make is not fully ported, just the
most common switches. Patches welcome.
A lot of the fine tuning niceties of ezmlm come as flag options to
the various ezmlm-* binaries. This library largely just deals with
ezmlm-make flags for global list behaviors. (For example, see the man
page for ezmlm-reject.) Patches are welcome if you'd like these sorts
of miscellanous things included.
## License

View file

@ -45,12 +45,12 @@ spec = Gem::Specification.new do |s|
# s.executables = %w[]
s.description = <<-EOF
This is a ruby interface for interacting with ezmlm-idx, an email list
manager for use with the Qmail MTA. (The -idx provides an extended
feature set over the initial ezmlm environment), and the messages contained therein.
manager for use with the Qmail MTA, and the messages contained therein.
(The -idx provides an extended feature set over the original ezmlm
environment.)
EOF
s.required_ruby_version = '>= 2'
s.required_ruby_version = '>= 2.1'
s.add_dependency 'loggability', "~> 0.13"
s.add_dependency 'mail', "~> 2.6"
end
@ -115,4 +115,6 @@ end
### M A N I F E S T
########################################################################
__END__
lib/ezmlm/list.rb
lib/ezmlm.rb

View file

@ -1,7 +1,7 @@
#!/usr/bin/ruby
# vim: set nosta noet ts=4 sw=4:
#
# A Ruby programmatic interface to the ezmlm-idx mailing list system
# A Ruby interface to the ezmlm-idx mailing list system.
#
# == Version
#
@ -27,19 +27,19 @@ module Ezmlm
module_function
###############
### Find all directories that look like an Ezmlm list directory under the specified +listsdir+
### and return Pathname objects for each.
### Find all directories that look like an Ezmlm list directory under
### the specified +listsdir+ and return Pathname objects for each.
###
def find_directories( listsdir )
listsdir = Pathname.new( listsdir )
return Pathname.glob( listsdir + '*' ).select do |entry|
return Pathname.glob( listsdir + '*' ).sort.select do |entry|
entry.directory? && ( entry + 'mailinglist' ).exist?
end
end
### Iterate over each directory that looks like an Ezmlm list in the specified +listsdir+ and
### yield it as an Ezmlm::List object.
### Iterate over each directory that looks like an Ezmlm list in the
### specified +listsdir+ and yield it as an Ezmlm::List object.
###
def each_list( listsdir )
find_directories( listsdir ).each do |entry|

View file

@ -77,12 +77,15 @@ class Ezmlm::List
end
### Return the number of messages in the list archive.
### Returns +true+ if +address+ is a subscriber to this list.
###
def message_count
count = self.read( 'archnum' )
return count ? Integer( count ) : 0
def include?( addr, section: nil )
addr.downcase!
file = self.subscription_dir( section ) + self.hashchar( addr )
return false unless file.exist?
return file.read.scan( /T([^\0]+)\0/ ).flatten.include?( addr )
end
alias_method :is_subscriber?, :include?
### Fetch a sorted Array of the email addresses for all of the list's
@ -93,38 +96,6 @@ class Ezmlm::List
end
### Returns an Array of email addresses of people responsible for
### moderating subscription of a closed list.
###
def moderators
return self.read_subscriber_dir( 'mod' )
end
### Subscribe +addr+ to the list as a Moderator.
###
def add_moderator( *addr )
return self.subscribe( *addr, section: 'mod' )
end
### Remove +addr+ from the list as a Moderator.
###
def remove_moderator( *addr )
return self.unsubscribe( *addr, section: 'mod' )
end
### Returns +true+ if +address+ is a subscriber to this list.
###
def include?( addr )
addr.downcase!
file = self.subscription_dir + self.hashchar( addr )
return false unless file.exist?
return file.read.scan( /T([^\0]+)\0/ ).flatten.include?( addr )
end
### Subscribe +addr+ to the list within +section+.
###
def subscribe( *addr, section: nil )
@ -149,6 +120,7 @@ class Ezmlm::List
end
end
end
alias_method :add_subscriber, :subscribe
### Unsubscribe +addr+ from the list within +section+.
@ -157,7 +129,7 @@ class Ezmlm::List
addr.each do |address|
address.downcase!
file = self.subscribers_dir( section ) + self.hashchar( address )
file = self.subscription_dir( section ) + self.hashchar( address )
self.with_safety do
next unless file.exist?
addresses = file.read.scan( /T([^\0]+)\0/ ).flatten
@ -173,65 +145,513 @@ class Ezmlm::List
end
end
end
alias_method :remove_subscriber, :unsubscribe
=begin
### Return the Date parsed from the last post to the list.
### Returns an Array of email addresses of people responsible for
### moderating subscription of a closed list.
###
def last_message_date
mail = self.last_post or return nil
return mail.date
def moderators
return self.read_subscriber_dir( 'mod' )
end
### Returns +true+ if +address+ is a moderator.
###
def is_moderator?( addr )
return self.include?( addr, section: 'mod' )
end
### Subscribe +addr+ to the list as a Moderator.
###
def add_moderator( *addr )
return self.subscribe( *addr, section: 'mod' )
end
### Remove +addr+ from the list as a Moderator.
###
def remove_moderator( *addr )
return self.unsubscribe( *addr, section: 'mod' )
end
### Return the author of the last post to the list.
### Returns an Array of email addresses denied access
### to the list.
###
def last_message_author
mail = self.last_post or return nil
return mail.from
def blacklisted
return self.read_subscriber_dir( 'deny' )
end
### Returns +true+ if +address+ is disallowed from participating.
###
def is_blacklisted?( addr )
return self.include?( addr, section: 'deny' )
end
### Blacklist +addr+ from the list.
###
def add_blacklisted( *addr )
return self.subscribe( *addr, section: 'deny' )
end
### Remove +addr+ from the blacklist.
###
def remove_blacklisted( *addr )
return self.unsubscribe( *addr, section: 'deny' )
end
### Returns +true+ if subscription to the list is moderated.
### Returns an Array of email addresses that act like
### regular subscribers for user-post only lists.
###
def closed?
return (self.listdir + 'modsub').exist? || (self.listdir + 'remote').exist?
def allowed
return self.read_subscriber_dir( 'allow' )
end
### Returns +true+ if +address+ is given the same benefits as a
### regular subscriber for user-post only lists.
###
def is_allowed?( addr )
return self.include?( addr, section: 'allow' )
end
### Add +addr+ to allow posting to user-post only lists,
### when +addr+ isn't a subscriber.
###
def add_allowed( *addr )
return self.subscribe( *addr, section: 'allow' )
end
### Remove +addr+ from the allowed list.
###
def remove_allowed( *addr )
return self.unsubscribe( *addr, section: 'allow' )
end
### Returns +true+ if posting to the list is moderated.
### Returns +true+ if message threading is enabled.
###
def threaded?
return ( self.listdir + 'threaded' ).exist?
end
### Disable or enable message threading.
###
### This automatically builds message indexes and thread
### information on an incoming message.
###
def threaded=( enable=true )
if enable
self.touch( 'threaded' )
else
self.unlink( 'threaded' )
end
end
### Returns +true+ if the list is configured to respond
### to remote mangement requests.
###
def public?
return ( self.listdir + 'public' ).exist?
end
### Disable or enable remote management requests.
###
def public=( enable=true )
if enable
self.touch( 'public' )
else
self.unlink( 'public' )
end
end
### Returns +true+ if the list is not configured to respond
### to remote mangement requests.
###
def private?
return ! self.public?
end
### Disable or enable remote management requests.
###
def private=( enable=false )
self.public = ! enable
end
### Returns +true+ if the list supports remote administration
### subscribe/unsubscribe requests from moderators.
###
def remote_subscriptions?
return ( self.listdir + 'remote' ).exist?
end
### Disable or enable remote subscription requests.
###
def remote_subscriptions=( enable=false )
if enable
self.touch( 'remote' )
else
self.unlink( 'remote' )
end
end
### Returns +true+ if list subscription requests require moderator
### approval.
###
def moderated_subscriptions?
return ( self.listdir + 'modsub' ).exist?
end
### Disable or enable subscription moderation.
###
def moderated_subscriptions=( enable=false )
if enable
self.touch( 'modsub' )
else
self.unlink( 'modsub' )
end
end
### Returns +true+ if message moderation is enabled.
###
def moderated?
return (self.listdir + 'modpost').exist?
return ( self.listdir + 'modpost' ).exist?
end
### Disable or enable message moderation.
###
### This has special meaning when combined with user_post_only setting.
### Lists act as unmoderated for subscribers, and posts from unknown
### addresses go to moderation.
###
def moderated=( enable=false )
if enable
self.touch( 'modpost' )
self.touch( 'noreturnposts' ) if self.user_posts_only?
else
self.unlink( 'modpost' )
self.unlink( 'noreturnposts' ) if self.user_posts_only?
end
end
### Return a Mail::Message object loaded from the last post to the list. Returns
### +nil+ if there are no archived posts.
### Returns +true+ if posting is only allowed by moderators.
###
def moderator_posts_only?
return ( self.listdir + 'modpostonly' ).exist?
end
### Disable or enable moderation only posts.
###
def moderator_posts_only=( enable=false )
if enable
self.touch( 'modpostonly' )
else
self.unlink( 'modpostonly' )
end
end
### Returns +true+ if posting is only allowed by subscribers.
###
def user_posts_only?
return ( self.listdir + 'subpostonly' ).exist?
end
### Disable or enable user only posts.
### This is easily defeated, moderated lists are preferred.
###
### This has special meaning for moderated lists. Lists act as
### unmoderated for subscribers, and posts from unknown addresses
### go to moderation.
###
def user_posts_only=( enable=false )
if enable
self.touch( 'subpostonly' )
self.touch( 'noreturnposts' )if self.moderated?
else
self.unlink( 'subpostonly' )
self.unlink( 'noreturnposts' ) if self.moderated?
end
end
### Returns +true+ if message archival is enabled.
###
def archived?
return ( self.listdir + 'archived' ).exist? || ( self.listdir + 'indexed' ).exist?
end
### Disable or enable message archiving (and indexing.)
###
def archive=( enable=true )
if enable
self.touch( 'archived' )
self.touch( 'indexed' )
else
self.unlink( 'archived' )
self.unlink( 'indexed' )
end
end
### Returns +true+ if the message archive is accessible only to
### moderators.
###
def private_archive?
return ( self.listdir + 'modgetonly' ).exist?
end
### Disable or enable private access to the archive.
###
def private_archive=( enable=true )
if enable
self.touch( 'modgetonly' )
else
self.unlink( 'modgetonly' )
end
end
### Returns +true+ if the message archive is accessible to anyone.
###
def public_archive?
return ! self.private_archive?
end
### Returns +true+ if the message archive is accessible only to
### list subscribers.
###
def guarded_archive?
return ( self.listdir + 'subgetonly' ).exist?
end
### Disable or enable loimited access to the archive.
###
def guarded_archive=( enable=true )
if enable
self.touch( 'subgetonly' )
else
self.unlink( 'subgetonly' )
end
end
### Returns +true+ if message digests are enabled.
###
def digested?
return ( self.listdir + 'digested' ).exist?
end
### Disable or enable message digesting.
###
def digest=( enable=true )
if enable
self.touch( 'digested' )
else
self.unlink( 'digested' )
end
end
### If the list is digestable, trigger the digest after this amount
### of message body since the latest digest, in kbytes.
###
### See: ezmlm-tstdig(1)
###
def digest_kbytesize
size = self.read( 'digsize' ).to_i
return size.zero? ? 64 : size
end
### If the list is digestable, trigger the digest after this amount
### of message body since the latest digest, in kbytes.
###
### See: ezmlm-tstdig(1)
###
def digest_kbytesize=( size=64 )
self.write( 'digsize' ) {|f| f.puts size.to_i }
end
### If the list is digestable, trigger the digest after this many
### messages have accumulated since the latest digest.
###
### See: ezmlm-tstdig(1)
###
def digest_count
count = self.read( 'digcount' ).to_i
return count.zero? ? 30 : count
end
### If the list is digestable, trigger the digest after this many
### messages have accumulated since the latest digest.
###
### See: ezmlm-tstdig(1)
###
def digest_count=( count=30 )
self.write( 'digcount' ) {|f| f.puts count.to_i }
end
### If the list is digestable, trigger the digest after this much
### time has passed since the last digest, in hours.
###
### See: ezmlm-tstdig(1)
###
def digest_timeout
hours = self.read( 'digtime' ).to_i
return hours.zero? ? 48 : hours
end
### If the list is digestable, trigger the digest after this much
### time has passed since the last digest, in hours.
###
### See: ezmlm-tstdig(1)
###
def digest_timeout=( hours=48 )
self.write( 'digtime' ) {|f| f.puts hours.to_i }
end
### Returns +true+ if the list requires subscriptions to be
### confirmed. AKA "help" mode if disabled.
###
def confirm_subscriptions?
return ! ( self.listdir + 'nosubconfirm' ).exist?
end
### Disable or enable subscription confirmation.
### AKA "help" mode if disabled.
###
def confirm_subscriptions=( enable=true )
if enable
self.unlink( 'nosubconfirm' )
else
self.touch( 'nosubconfirm' )
end
end
### Returns +true+ if the list requires unsubscriptions to be
### confirmed. AKA "jump" mode.
###
def confirm_unsubscriptions?
return ! ( self.listdir + 'nounsubconfirm' ).exist?
end
### Disable or enable unsubscription confirmation.
### AKA "jump" mode.
###
def confirm_unsubscriptions=( enable=true )
if enable
self.unlink( 'nounsubconfirm' )
else
self.touch( 'nounsubconfirm' )
end
end
### Returns +true+ if the list requires regular message postings
### to be confirmed by the original sender.
###
def confirm_postings?
return ( self.listdir + 'confirmpost' ).exist?
end
### Disable or enable message confirmation.
###
def confirm_postings=( enable=false )
if enable
self.touch( 'confirmpost' )
else
self.unlink( 'confirmpost' )
end
end
### Returns +true+ if the list allows moderators to
### fetch a subscriber list remotely.
###
def allow_remote_listing?
return ( self.listdir + 'modcanlist' ).exist?
end
### Disable or enable the ability for moderators to
### remotely fetch a subscriber list.
###
def allow_remote_listing=( enable=false )
if enable
self.touch( 'modcanlist' )
else
self.unlink( 'modcanlist' )
end
end
### Returns +true+ if the list automatically manages
### bouncing subscriber addresses.
###
def bounce_warnings?
return ! ( self.listdir + 'nowarn' ).exist?
end
### Disable or enable automatic bounce probes and warnings.
###
def bounce_warnings=( enable=true )
if enable
self.unlink( 'nowarn' )
else
self.touch( 'nowarn' )
end
end
### Return the maximum message size, in bytes. Messages larger than
### this size will be rejected.
###
### See: ezmlm-reject(1)
###
def maximum_message_size
size = self.read( 'msgsize' )
return size ? size.split( ':' ).first.to_i : 0
end
### Set the maximum message size, in bytes. Messages larger than
### this size will be rejected.
###
### See: ezmlm-reject(1)
###
def maximum_message_size=( size=307200 )
if size.to_i.zero?
self.unlink( 'msgsize' )
else
self.write( 'msgsize' ) {|f| f.puts "#{size.to_i}:0" }
end
end
### Return the number of messages in the list archive.
###
def message_count
count = self.read( 'archnum' )
return count ? Integer( count ) : 0
end
### Returns the last message to the list as a Mail::Message, if
### archiving was enabled.
###
def last_post
archivedir = self.listdir + 'archive'
return nil unless archivedir.exist?
num = self.message_count
return if num.zero?
# Find the last numbered directory under the archive dir
last_archdir = Pathname.glob( archivedir + '[0-9]*' ).
sort_by {|pn| Integer(pn.basename.to_s) }.last
hashdir = num / 100
message = "%02d" % [ num % 100 ]
return nil unless last_archdir
post = self.listdir + 'archive' + hashdir.to_s + message.to_s
return unless post.exist?
# Find the last numbered file under the last numbered directory we found
# above.
last_post_path = Pathname.glob( last_archdir + '[0-9]*' ).
sort_by {|pn| pn.basename.to_s }.last
raise RuntimeError, "unexpectedly empty archive directory '%s'" % [ last_archdir ] \
unless last_post_path
require 'pry'
binding.pry
last_post = TMail::Mail.load( last_post_path.to_s )
return Mail.read( post.to_s )
end
=end
#########
@ -244,7 +664,7 @@ class Ezmlm::List
### Older ezmlm didn't lowercase addresses, anything within the last
### decade did. We're not going to worry about compatibility there.
###
### (See subhash.c in the ezmlm source.)
### See: subhash.c in the ezmlm source.
###
def subhash( addr )
h = 5381
@ -277,12 +697,51 @@ class Ezmlm::List
end
### Overwrite +file+ safely, yielding the open filehandle to the
### block. Set the new file to correct ownership and permissions.
###
def write( file, &block )
file = self.listdir + file unless file.is_a?( Pathname )
self.with_safety do
file.open( 'w' ) do |f|
yield( f )
end
stat = self.listdir.stat
file.chown( stat.uid, stat.gid )
file.chmod( 0600 )
end
end
### Simply create an empty file, safely.
###
def touch( file )
self.write( file ) {}
end
### Delete +file+ safely.
###
def unlink( file )
file = self.listdir + file unless file.is_a?( Pathname )
return unless file.exist?
self.with_safety do
file.unlink
end
end
### Return a Pathname to a subscription directory.
###
def subscription_dir( section=nil )
section = nil if section && ! SUBSCRIPTION_DIRS.include?( section )
if section
unless SUBSCRIPTION_DIRS.include?( section )
raise "Invalid subscription dir: %s, must be one of: %s" % [
section,
SUBSCRIPTION_DIRS.join( ', ' )
]
end
return self.listdir + section + 'subscribers'
else
return self.listdir + 'subscribers'
@ -290,8 +749,8 @@ class Ezmlm::List
end
### Read the hashed subscriber email addresses from the specified +directory+ and return them in
### an Array.
### Read the hashed subscriber email addresses from the specified
### +directory+ and return them in an Array.
###
def read_subscriber_dir( section=nil )
directory = self.subscription_dir( section )

1
spec/data/testlist/Log Normal file
View file

@ -0,0 +1 @@
1485642996 +manual mahlon@laika.com

View file

View file

@ -0,0 +1,2 @@
|/usr/local/bin/ezmlm-weed
|/usr/local/bin/ezmlm-return -D '/tmp/woo/test'

View file

@ -0,0 +1,5 @@
|/usr/local/bin/ezmlm-weed
|if test -f '/tmp/woo/test/subpostonly' -a -f '/tmp/woo/test/modpost'; then /usr/local/bin/ezmlm-confirm '/tmp/woo/test' /usr/local/bin/ezmlm-gate -Y '/tmp/woo/test' . digest allow mod; else /usr/local/bin/ezmlm-confirm '/tmp/woo/test' /usr/local/bin/ezmlm-store -Y '/tmp/woo/test'; fi
|if test -f '/tmp/woo/test/threaded'; then /usr/local/bin/ezmlm-archive '/tmp/woo/test' || exit 0; fi
|/usr/local/bin/ezmlm-clean '/tmp/woo/test' || exit 0
|/usr/local/bin/ezmlm-warn '/tmp/woo/test' || exit 0

View file

@ -0,0 +1 @@
10

View file

@ -0,0 +1,2 @@
|/usr/local/bin/ezmlm-weed
|/usr/local/bin/ezmlm-return -d '/tmp/woo/test'

View file

View file

View file

@ -0,0 +1 @@

1
spec/data/testlist/dot Normal file
View file

@ -0,0 +1 @@
/tmp/woo/.qmail-test

10
spec/data/testlist/editor Normal file
View file

@ -0,0 +1,10 @@
|if test ! -f '/tmp/woo/test/sublist'; then /usr/local/bin/ezmlm-reject '/tmp/woo/test'; fi
|if test ! -f '/tmp/woo/test/sublist'; then /usr/local/bin/ezmlm-checksub -n '/tmp/woo/test' deny; fi
|if test ! -f '/tmp/woo/test/subpostonly'; then /usr/local/bin/ezmlm-store '/tmp/woo/test'; fi
|if test -f '/tmp/woo/test/subpostonly' -a ! -f '/tmp/woo/test/modpost'; then /usr/local/bin/ezmlm-checksub '/tmp/woo/test' . digest allow mod && /usr/local/bin/ezmlm-store '/tmp/woo/test'; fi
|if test -f '/tmp/woo/test/subpostonly' -a -f '/tmp/woo/test/modpost' -a ! -f '/tmp/woo/test/confirmpost'; then /usr/local/bin/ezmlm-gate '/tmp/woo/test' . digest allow mod; fi
|if test -f '/tmp/woo/test/subpostonly' -a -f '/tmp/woo/test/modpost' -a -f '/tmp/woo/test/confirmpost'; then /usr/local/bin/ezmlm-store '/tmp/woo/test'; fi
|/usr/local/bin/ezmlm-clean '/tmp/woo/test' || exit 0
|if test -f '/tmp/woo/test/threaded'; then /usr/local/bin/ezmlm-archive '/tmp/woo/test' || exit 0; fi
|/usr/local/bin/ezmlm-warn '/tmp/woo/test' || exit 0
|if test -e '/tmp/woo/test/digested' && /usr/local/bin/ezmlm-tstdig '/tmp/woo/test'; then /usr/local/bin/ezmlm-get '/tmp/woo/test' || exit 0; fi

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,7 @@
Sender: <<#l#>@<#h#>>
Precedence: bulk
X-No-Archive: yes
List-Post: <mailto:test@lists.laika.com>
List-Help: <mailto:<#l#>-help@<#h#>>
List-Unsubscribe: <mailto:<#l#>-unsubscribe@<#h#>>
List-Subscribe: <mailto:<#l#>-subscribe@<#h#>>

View file

@ -0,0 +1,11 @@
return-path
return-receipt-to
content-length
precedence
x-confirm-reading-to
x-pmrqc
list-subscribe
list-unsubscribe
list-help
list-post
sender

View file

BIN
spec/data/testlist/key Normal file

Binary file not shown.

0
spec/data/testlist/lock Normal file
View file

View file

View file

@ -0,0 +1,7 @@
|/usr/local/bin/ezmlm-weed
|/usr/local/bin/ezmlm-get '/tmp/woo/test'
|/usr/local/bin/ezmlm-split '/tmp/woo/test'
|/usr/local/bin/ezmlm-request '/tmp/woo/test'
|/usr/local/bin/ezmlm-manage '/tmp/woo/test'
|/usr/local/bin/ezmlm-clean '/tmp/woo/test' || exit 0
|/usr/local/bin/ezmlm-warn '/tmp/woo/test' || exit 0

View file

@ -0,0 +1,5 @@
|/usr/local/bin/ezmlm-weed
|/usr/local/bin/ezmlm-moderate '/tmp/woo/test'
|if test -f '/tmp/woo/test/threaded'; then /usr/local/bin/ezmlm-archive '/tmp/woo/test' || exit 0; fi
|/usr/local/bin/ezmlm-clean '/tmp/woo/test' || exit 0
|/usr/local/bin/ezmlm-warn '/tmp/woo/test' || exit 0

View file

@ -0,0 +1 @@
lists.syrup.info

View file

@ -0,0 +1 @@
waffle-lovers

1
spec/data/testlist/owner Normal file
View file

@ -0,0 +1 @@
/tmp/woo/test/Mailbox

View file

View file

@ -12,545 +12,334 @@ require 'ezmlm'
describe Ezmlm::List do
# Testing constants
TEST_LISTDIR = Pathname.new( 'list' )
TEST_LIST_NAME = 'waffle-lovers'
TEST_LIST_HOST = 'lists.syrup.info'
TEST_OWNER = 'listowner@rumpus-the-whale.info'
TEST_CUSTOM_MODERATORS_DIR = '/foo/bar/clowns'
before( :each ) do
@listdir = make_listdir()
end
TEST_SUBSCRIBERS = %w[
pete.chaffee@toadsmackers.com
dolphinzombie@alahalohamorra.com
piratebanker@yahoo.com
]
TEST_MODERATORS = %w[
dolphinzombie@alahalohamorra.com
]
TEST_CONFIG = <<-"EOF".gsub( /^\t+/, '' )
F:-aBCDeFGHijKlMnOpQrStUVWXYZ
X:
D:/var/qmail/alias/lists/waffle-lovers/
T:/var/qmail/alias/.qmail-waffle-lovers
L:#{TEST_LIST_NAME}
H:#{TEST_LIST_HOST}
C:
0:
3:
4:
5:#{TEST_OWNER}
6:
7:
8:
9:
EOF
it "can create a list"
it "can add a new subscriber"
it "can remove a current subscriber"
it "can edit the list's text files"
###
### List manager functions
###
describe "list manager functions" do
before( :each ) do
@listpath = TEST_LISTDIR.dup
@list = Ezmlm::List.new( @listpath )
end
it "can return the configured list name" do
allow(@list).to receive( :config ).and_return({ 'L' => :the_list_name })
expect(@list.name).to eq(:the_list_name)
end
it "can return the configured list host" do
allow(@list).to receive( :config ).and_return({ 'H' => :the_list_host })
expect(@list.host).to eq(:the_list_host)
end
it "can return the configured list address" do
allow(@list).to receive( :config ).and_return({ 'L' => TEST_LIST_NAME, 'H' => TEST_LIST_HOST })
expect(@list.address).to eq("%s@%s" % [ TEST_LIST_NAME, TEST_LIST_HOST ])
end
CONFIG_KEYS = %w[ F X D T L H C 0 3 4 5 6 7 8 9 ]
it "can fetch the list config as a Hash" do
config_path = double( "Mock config path" )
expect(@listpath).to receive( :+ ).with( 'config' ).and_return( config_path )
expect(config_path).to receive( :exist? ).and_return( true )
expect(config_path).to receive( :read ).and_return( TEST_CONFIG )
expect(@list.config).to be_an_instance_of( Hash )
expect(@list.config.size).to eq(CONFIG_KEYS.length)
expect(@list.config.keys).to include( *CONFIG_KEYS )
end
it "raises an error if the list config file doesn't exist" do
config_path = double( "Mock config path" )
expect(@listpath).to receive( :+ ).with( 'config' ).and_return( config_path )
expect(config_path).to receive( :exist? ).and_return( false )
expect {
@list.config
}.to raise_error( RuntimeError, /does not exist/ )
end
it "can return a list of subscribers' email addresses" do
subscribers_dir = TEST_LISTDIR + 'subscribers'
expectation = expect(Pathname).to receive( :glob ).with( subscribers_dir + '*' )
TEST_SUBSCRIBERS.each do |email|
mock_subfile = double( "Mock subscribers file for '#{email}'" )
expect(mock_subfile).to receive( :read ).and_return( "T#{email}\0" )
expectation.and_yield( mock_subfile )
end
subscribers = @list.subscribers
expect(subscribers.size).to eq(TEST_SUBSCRIBERS.length)
expect(subscribers).to include( *TEST_SUBSCRIBERS )
end
### Subscriber moderation
it "knows that subscription moderation is enabled if the dir/modsub file exists" do
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).and_return( true )
expect(@list).to be_closed()
end
it "knows that subscription moderation is enabled if the dir/remote file exists" do
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).and_return( false )
remote_path_obj = double( "Mock 'remote' path object" )
expect(@listpath).to receive( :+ ).with( 'remote' ).and_return( remote_path_obj )
expect(remote_path_obj).to receive( :exist? ).and_return( true )
expect(@list).to be_closed()
end
it "knows that subscription moderation is disabled if neither the dir/modsub nor " +
"dir/remote files exist" do
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).and_return( false )
remote_path_obj = double( "Mock 'remote' path object" )
expect(@listpath).to receive( :+ ).with( 'remote' ).and_return( remote_path_obj )
expect(remote_path_obj).to receive( :exist? ).and_return( false )
expect(@list).not_to be_closed()
end
it "returns an empty array of subscription moderators for an open list" do
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).and_return( false )
remote_path_obj = double( "Mock 'remote' path object" )
expect(@listpath).to receive( :+ ).with( 'remote' ).and_return( remote_path_obj )
expect(remote_path_obj).to receive( :exist? ).and_return( false )
expect(@list.subscription_moderators).to be_empty()
end
it "can return a list of subscription moderators' email addresses" do
# Test the moderation config files for existence
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).twice.and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).twice.and_return( true )
remote_path_obj = double( "Mock 'remote' path object" )
expect(@listpath).to receive( :+ ).with( 'remote' ).and_return( remote_path_obj )
expect(remote_path_obj).to receive( :exist? ).once.and_return( true )
# Try to read directory names from both config files
expect(modsub_path_obj).to receive( :read ).with( 1 ).and_return( nil )
expect(remote_path_obj).to receive( :read ).with( 1 ).and_return( nil )
# Read subscribers from the default directory
subscribers_dir = double( "Mock moderator subscribers directory" )
expect(@listpath).to receive( :+ ).with( 'mod/subscribers' ).and_return( subscribers_dir )
expect(subscribers_dir).to receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = expect(Pathname).to receive( :glob ).with( :mod_sub_dir )
TEST_MODERATORS.each do |email|
mock_subfile = double( "Mock subscribers file for '#{email}'" )
expect(mock_subfile).to receive( :read ).and_return( "T#{email}\0" )
expectation.and_yield( mock_subfile )
end
mods = @list.subscription_moderators
expect(mods.size).to eq(TEST_MODERATORS.length)
expect(mods).to include( *TEST_MODERATORS )
end
it "can return a list of subscription moderators' email addresses when the moderators " +
"directory has been customized" do
# Test the moderation config files for existence
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).twice.and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).twice.and_return( true )
expect(@listpath).to receive( :+ ).with( 'remote' )
# Try to read directory names from both config files
expect(modsub_path_obj).to receive( :read ).with( 1 ).and_return( '/' )
expect(modsub_path_obj).to receive( :read ).with().and_return( TEST_CUSTOM_MODERATORS_DIR )
custom_mod_path = double( "Mock path object for customized moderator dir" )
expect(Pathname).to receive( :new ).with( TEST_CUSTOM_MODERATORS_DIR ).and_return( custom_mod_path )
# Read subscribers from the default file
expect(custom_mod_path).to receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = expect(Pathname).to receive( :glob ).with( :mod_sub_dir )
TEST_MODERATORS.each do |email|
mock_subfile = double( "Mock subscribers file for '#{email}'" )
expect(mock_subfile).to receive( :read ).and_return( "T#{email}\0" )
expectation.and_yield( mock_subfile )
end
mods = @list.subscription_moderators
expect(mods.size).to eq(TEST_MODERATORS.length)
expect(mods).to include( *TEST_MODERATORS )
end
it "can get a list of modererators when remote subscription moderation is enabled" +
" and the modsub configuration is empty" do
# Test the moderation config files for existence
modsub_path_obj = double( "Mock 'modsub' path object" )
expect(@listpath).to receive( :+ ).with( 'modsub' ).twice.and_return( modsub_path_obj )
expect(modsub_path_obj).to receive( :exist? ).twice.and_return( false )
remote_path_obj = double( "Mock 'remote' path object" )
expect(@listpath).to receive( :+ ).with( 'remote' ).twice.and_return( remote_path_obj )
expect(remote_path_obj).to receive( :exist? ).twice.and_return( true )
# Try to read directory names from both config files
expect(remote_path_obj).to receive( :read ).with( 1 ).and_return( '/' )
expect(remote_path_obj).to receive( :read ).with().and_return( TEST_CUSTOM_MODERATORS_DIR )
custom_mod_path = double( "Mock path object for customized moderator dir" )
expect(Pathname).to receive( :new ).with( TEST_CUSTOM_MODERATORS_DIR ).and_return( custom_mod_path )
# Read subscribers from the default file
expect(custom_mod_path).to receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = expect(Pathname).to receive( :glob ).with( :mod_sub_dir )
TEST_MODERATORS.each do |email|
mock_subfile = double( "Mock subscribers file for '#{email}'" )
expect(mock_subfile).to receive( :read ).and_return( "T#{email}\0" )
expectation.and_yield( mock_subfile )
end
mods = @list.subscription_moderators
expect(mods.size).to eq(TEST_MODERATORS.length)
expect(mods).to include( *TEST_MODERATORS )
end
### Message moderation
it "knows that subscription moderation is enabled if the dir/modpost file exists" do
modpost_path_obj = double( "Mock 'modpost' path object" )
expect(@listpath).to receive( :+ ).with( 'modpost' ).and_return( modpost_path_obj )
expect(modpost_path_obj).to receive( :exist? ).and_return( true )
expect(@list).to be_moderated()
end
it "knows that subscription moderation is disabled if the dir/modpost file doesn't exist" do
modpost_path_obj = double( "Mock 'modpost' path object" )
expect(@listpath).to receive( :+ ).with( 'modpost' ).and_return( modpost_path_obj )
expect(modpost_path_obj).to receive( :exist? ).and_return( false )
expect(@list).not_to be_moderated()
end
it "returns an empty array of message moderators for an open list" do
modpost_path_obj = double( "Mock 'modpost' path object" )
expect(@listpath).to receive( :+ ).with( 'modpost' ).and_return( modpost_path_obj )
expect(modpost_path_obj).to receive( :exist? ).and_return( false )
expect(@list.message_moderators).to be_empty()
end
it "can return a list of message moderators' email addresses" do
# Test the moderation config file for existence
modpost_path_obj = double( "Mock 'modpost' path object" )
expect(@listpath).to receive( :+ ).with( 'modpost' ).twice.and_return( modpost_path_obj )
expect(modpost_path_obj).to receive( :exist? ).twice.and_return( true )
# Try to read directory names from the config file
expect(modpost_path_obj).to receive( :read ).with( 1 ).and_return( nil )
# Read subscribers from the default directory
subscribers_dir = double( "Mock moderator subscribers directory" )
expect(@listpath).to receive( :+ ).with( 'mod/subscribers' ).and_return( subscribers_dir )
expect(subscribers_dir).to receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = expect(Pathname).to receive( :glob ).with( :mod_sub_dir )
TEST_MODERATORS.each do |email|
mock_subfile = double( "Mock subscribers file for '#{email}'" )
expect(mock_subfile).to receive( :read ).and_return( "T#{email}\0" )
expectation.and_yield( mock_subfile )
end
mods = @list.message_moderators
expect(mods.size).to eq(TEST_MODERATORS.length)
expect(mods).to include( *TEST_MODERATORS )
end
it "can return a list of message moderators' email addresses when the moderators " +
"directory has been customized" do
# Test the moderation config files for existence
modpost_path_obj = double( "Mock 'modpost' path object" )
expect(@listpath).to receive( :+ ).with( 'modpost' ).twice.and_return( modpost_path_obj )
expect(modpost_path_obj).to receive( :exist? ).twice.and_return( true )
# Try to read directory names from both config files
expect(modpost_path_obj).to receive( :read ).with( 1 ).and_return( '/' )
expect(modpost_path_obj).to receive( :read ).with().and_return( TEST_CUSTOM_MODERATORS_DIR )
custom_mod_path = double( "Mock path object for customized moderator dir" )
expect(Pathname).to receive( :new ).with( TEST_CUSTOM_MODERATORS_DIR ).and_return( custom_mod_path )
# Read subscribers from the default file
expect(custom_mod_path).to receive( :+ ).with( '*' ).and_return( :mod_sub_dir )
expectation = expect(Pathname).to receive( :glob ).with( :mod_sub_dir )
TEST_MODERATORS.each do |email|
mock_subfile = double( "Mock subscribers file for '#{email}'" )
expect(mock_subfile).to receive( :read ).and_return( "T#{email}\0" )
expectation.and_yield( mock_subfile )
end
mods = @list.message_moderators
expect(mods.size).to eq(TEST_MODERATORS.length)
expect(mods).to include( *TEST_MODERATORS )
end
### List owner
it "returns nil when the list doesn't have an owner in its config" do
allow(@list).to receive( :config ).and_return({ '5' => nil })
expect(@list.owner).to eq(nil)
end
it "can return the email address of the list owner" do
allow(@list).to receive( :config ).and_return({ '5' => TEST_OWNER })
expect(@list.owner).to eq(TEST_OWNER)
end
after( :each ) do
rm_r( @listdir )
end
let( :list ) do
described_class.new( @listdir )
end
###
### Archive functions
###
describe "archive functions" do
it "can return the list name" do
expect( list.name ).to eq( TEST_LIST_NAME )
end
before( :each ) do
@listpath = TEST_LISTDIR.dup
@list = Ezmlm::List.new( @listpath )
end
it "can return the list host" do
expect( list.host ).to eq( TEST_LIST_HOST )
end
it "can return the list address" do
expect( list.address ).to eq( TEST_LIST_NAME + '@' + TEST_LIST_HOST )
end
it "can return the count of archived posts" do
numpath_obj = double( "num file path object" )
expect(@listpath).to receive( :+ ).with( 'num' ).and_return( numpath_obj )
expect(numpath_obj).to receive( :exist? ).and_return( true )
expect(numpath_obj).to receive( :read ).and_return( "1723:123123123" )
expect(@list.message_count).to eq(1723)
end
it "can return the count of archived posts to a list that hasn't been posted to" do
numpath_obj = double( "num file path object" )
expect(@listpath).to receive( :+ ).with( 'num' ).and_return( numpath_obj )
expect(numpath_obj).to receive( :exist? ).and_return( false )
expect(@list.message_count).to eq(0)
end
TEST_ARCHIVE_DIR = TEST_LISTDIR + 'archive'
TEST_ARCHIVE_SUBDIRS = %w[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 ]
TEST_POST_FILES = %w[ 00 01 02 03 04 05 06 07 08 09 10 11 12 13 ]
before( :each ) do
@archive_dir = TEST_ARCHIVE_DIR.dup
@archive_subdirs = TEST_ARCHIVE_SUBDIRS.dup
@archive_subdir_paths = TEST_ARCHIVE_SUBDIRS.collect {|pn| TEST_ARCHIVE_DIR + pn }
@archive_post_paths = TEST_POST_FILES.collect {|pn|
TEST_ARCHIVE_DIR + TEST_ARCHIVE_SUBDIRS.last + pn
}
end
it "can return a TMail::Mail object parsed from the last archived post" do
# need to find the last message
archive_path_obj = double( "archive path" )
expect(@listpath).to receive( :+ ).with( 'archive' ).and_return( archive_path_obj )
expect(archive_path_obj).to receive( :exist? ).and_return( true )
# Find the last numbered directory under the archive dir
expect(archive_path_obj).to receive( :+ ).with( '[0-9]*' ).
and_return( :archive_dir_globpath )
expect(Pathname).to receive( :glob ).with( :archive_dir_globpath ).
and_return( @archive_subdir_paths )
# Find the last numbered file under the last numbered directory we found
# above.
expect(@archive_subdir_paths.last).to receive( :+ ).with( '[0-9]*' ).
and_return( :archive_post_pathglob )
expect(Pathname).to receive( :glob ).with( :archive_post_pathglob ).
and_return( @archive_post_paths )
expect(TMail::Mail).to receive( :load ).with( @archive_post_paths.last.to_s ).
and_return( :mail_object )
expect(@list.last_post).to eq(:mail_object)
end
it "returns nil for the last post if there is no archive directory for the list" do
archive_path_obj = double( "archive path" )
expect(@listpath).to receive( :+ ).with( 'archive' ).and_return( archive_path_obj )
expect(archive_path_obj).to receive( :exist? ).and_return( false )
expect(@list.last_post).to eq(nil)
end
it "returns nil for the last post if there haven't been any posts to the list" do
archive_path_obj = double( "archive path" )
mail_object = double( "Mock TMail object" )
expect(@listpath).to receive( :+ ).with( 'archive' ).and_return( archive_path_obj )
expect(archive_path_obj).to receive( :exist? ).and_return( true )
# Find the last numbered directory under the archive dir
expect(archive_path_obj).to receive( :+ ).with( '[0-9]*' ).
and_return( :archive_dir_globpath )
expect(Pathname).to receive( :glob ).with( :archive_dir_globpath ).and_return( [] )
expect(@list.last_post).to eq(nil)
end
it "raises a RuntimeError if the last archive directory doesn't have any messages in it" do
archive_path_obj = double( "archive path" )
mail_object = double( "Mock TMail object" )
expect(@listpath).to receive( :+ ).with( 'archive' ).and_return( archive_path_obj )
expect(archive_path_obj).to receive( :exist? ).and_return( true )
# Find the last numbered directory under the archive dir
expect(archive_path_obj).to receive( :+ ).with( '[0-9]*' ).
and_return( :archive_dir_globpath )
expect(Pathname).to receive( :glob ).with( :archive_dir_globpath ).
and_return( @archive_subdir_paths )
expect(@archive_subdir_paths.last).to receive( :+ ).with( '[0-9]*' ).
and_return( :archive_post_pathglob )
expect(Pathname).to receive( :glob ).with( :archive_post_pathglob ).
and_return( [] )
expect {
@list.last_post
}.to raise_error( RuntimeError, /unexpectedly empty/i )
end
it "can fetch the date of the last archived post" do
mail_object = double( "Mock TMail object" )
expect(@list).to receive( :last_post ).and_return( mail_object )
expect(mail_object).to receive( :date ).and_return( :the_message_date )
expect(@list.last_message_date).to eq(:the_message_date)
end
it "can fetch the date of the last archived post" do
mail_object = double( "Mock TMail object" )
expect(@list).to receive( :last_post ).and_return( mail_object )
expect(mail_object).to receive( :date ).and_return( :the_message_date )
expect(@list.last_message_date).to eq(:the_message_date)
end
it "can fetch the author of the last archived post" do
mail_object = double( "Mock TMail object" )
expect(@list).to receive( :last_post ).and_return( mail_object )
expect(mail_object).to receive( :from ).and_return( :the_message_author )
expect(@list.last_message_author).to eq(:the_message_author)
end
it "can fetch the subject of the last archived post" do
mail_object = double( "Mock TMail object" )
expect(@list).to receive( :last_post ).and_return( mail_object )
expect(mail_object).to receive( :from ).and_return( :the_message_author )
expect(@list.last_message_author).to eq(:the_message_author)
end
it "returns nil if the list owner isn't an email address" do
expect( list.owner ).to eq( nil )
end
it "can return an email address owner" do
expect( list ).to receive( :read ).with( 'owner' ).and_return( TEST_OWNER )
expect( list.owner ).to eq( TEST_OWNER )
end
it "can fetch the body of an archived post by message id"
it "can fetch the header of an archived post by message id"
it "can add a new subscriber" do
list.add_subscriber( *TEST_SUBSCRIBERS )
expect( list.is_subscriber?( TEST_SUBSCRIBERS.first ) ).to be_truthy
end
it "can return a hash of the subjects of all archived posts to message ids"
it "can return an Array of the subjects of all archived posts"
it "can return the list of subscibers" do
list.add_subscriber( *TEST_SUBSCRIBERS )
list.add_subscriber( 'notanemailaddress' )
expect( list.subscribers.length ).to eq( 3 )
expect( list.subscribers ).to include( TEST_SUBSCRIBERS.first )
end
it "can return a hash of the threads of all archived posts to message ids"
it "can return an Array of the threads of all archived posts"
it "can remove a current subscriber" do
list.add_subscriber( *TEST_SUBSCRIBERS )
list.remove_subscriber( 'notanemailaddress' )
list.remove_subscriber( TEST_MODERATORS.first )
expect( list.subscribers.length ).to eq( 2 )
end
it "can return a hash of the authors of all archived posts to message ids"
it "can return an Array of the authors of all archived posts"
it "can add a new moderator" do
list.add_moderator( *TEST_MODERATORS )
expect( list.is_moderator?( TEST_MODERATORS.first ) ).to be_truthy
end
it "can return the list of moderators" do
list.add_moderator( *TEST_MODERATORS )
expect( list.moderators.length ).to eq( 1 )
expect( list.moderators ).to include( TEST_MODERATORS.first )
end
it "can remove a current moderator" do
list.add_moderator( *TEST_MODERATORS )
list.remove_moderator( TEST_MODERATORS.first )
expect( list.moderators ).to be_empty
end
it "can add a blacklisted address" do
list.add_blacklisted( *TEST_MODERATORS )
expect( list.is_blacklisted?( TEST_MODERATORS.first ) ).to be_truthy
end
it "can return the list of blacklisted addresses" do
list.add_blacklisted( *TEST_MODERATORS )
expect( list.blacklisted.length ).to eq( 1 )
expect( list.blacklisted ).to include( TEST_MODERATORS.first )
end
it "can remove a blacklisted address" do
list.add_blacklisted( *TEST_MODERATORS )
list.remove_blacklisted( TEST_MODERATORS.first )
expect( list.blacklisted ).to be_empty
end
it "can add an allowed address" do
list.add_allowed( *TEST_MODERATORS )
expect( list.is_allowed?( TEST_MODERATORS.first ) ).to be_truthy
end
it "can return the list of allowed addresses" do
list.add_allowed( *TEST_MODERATORS )
expect( list.allowed.length ).to eq( 1 )
expect( list.allowed ).to include( TEST_MODERATORS.first )
end
it "can remove a allowed address" do
list.add_allowed( *TEST_MODERATORS )
list.remove_allowed( TEST_MODERATORS.first )
expect( list.allowed ).to be_empty
end
it 'can return the current threading state' do
expect( list.threaded? ).to be_falsey
end
it 'can set the threading state' do
list.threaded = true
expect( list.threaded? ).to be_truthy
end
it 'can return the current public/private state' do
expect( list.public? ).to be_truthy
expect( list.private? ).to be_falsey
end
it 'can set the privacy state' do
list.public = false
expect( list.public? ).to be_falsey
expect( list.private? ).to be_truthy
list.private = false
expect( list.private? ).to be_falsey
expect( list.public? ).to be_truthy
end
it 'can set the remote subscription state' do
expect( list.remote_subscriptions? ).to be_falsey
list.remote_subscriptions = true
expect( list.remote_subscriptions? ).to be_truthy
list.remote_subscriptions = false
expect( list.remote_subscriptions? ).to be_falsey
end
it 'can set subscription moderation state' do
expect( list.moderated_subscriptions? ).to be_falsey
list.moderated_subscriptions = true
expect( list.moderated_subscriptions? ).to be_truthy
list.moderated_subscriptions = false
expect( list.moderated_subscriptions? ).to be_falsey
end
it 'can set posting moderation state' do
expect( list.moderated? ).to be_falsey
list.moderated = true
expect( list.moderated? ).to be_truthy
list.moderated = false
expect( list.moderated? ).to be_falsey
end
it 'can set moderation-only posting' do
expect( list.moderator_posts_only? ).to be_falsey
list.moderator_posts_only = true
expect( list.moderator_posts_only? ).to be_truthy
list.moderator_posts_only = false
expect( list.moderator_posts_only? ).to be_falsey
end
it 'can set user-only posting' do
expect( list.user_posts_only? ).to be_falsey
list.user_posts_only = true
expect( list.user_posts_only? ).to be_truthy
list.user_posts_only = false
expect( list.user_posts_only? ).to be_falsey
end
it 'user+moderation together sets non-subscriber moderation' do
expect( list.user_posts_only? ).to be_falsey
expect( list.moderated? ).to be_falsey
list.moderated = true
list.user_posts_only = true
expect( list.listdir + 'noreturnposts' ).to exist
list.moderated = false
expect( list.listdir + 'noreturnposts' ).to_not exist
end
it 'can set archival status' do
expect( list.archived? ).to be_truthy
list.archive = false
expect( list.archived? ).to be_falsey
list.archive = true
expect( list.archived? ).to be_truthy
end
it 'can limit archive access to moderators only' do
expect( list.private_archive? ).to be_falsey
list.private_archive = true
expect( list.private_archive? ).to be_truthy
list.private_archive = false
expect( list.private_archive? ).to be_falsey
end
it 'can limit archive access to list subscribers only' do
expect( list.guarded_archive? ).to be_falsey
list.guarded_archive = true
expect( list.guarded_archive? ).to be_truthy
list.guarded_archive = false
expect( list.guarded_archive? ).to be_falsey
end
it 'can toggle digest status' do
expect( list.digested? ).to be_falsey
list.digest = true
expect( list.digested? ).to be_truthy
list.digest = false
expect( list.digested? ).to be_falsey
end
it 'returns a default digest kbyte size' do
expect( list.digest_kbytesize ).to eq( 64 )
end
it 'can set a new digest kbyte size' do
list.digest_kbytesize = 300
expect( list.digest_kbytesize ).to eq( 300 )
end
it 'returns a default digest message count' do
expect( list.digest_count ).to eq( 10 )
end
it 'can set a new digest message count' do
list.digest_count = 25
expect( list.digest_count ).to eq( 25 )
end
it 'returns a default digest timeout' do
expect( list.digest_timeout ).to eq( 48 )
end
it 'can set a new digest timeout' do
list.digest_timeout = 24
expect( list.digest_timeout ).to eq( 24 )
end
it 'can set subscription confirmation' do
expect( list.confirm_subscriptions? ).to be_truthy
list.confirm_subscriptions = false
expect( list.confirm_subscriptions? ).to be_falsey
list.confirm_subscriptions = true
expect( list.confirm_subscriptions? ).to be_truthy
end
it 'can set unsubscription confirmation' do
expect( list.confirm_unsubscriptions? ).to be_truthy
list.confirm_unsubscriptions = false
expect( list.confirm_unsubscriptions? ).to be_falsey
list.confirm_unsubscriptions = true
expect( list.confirm_unsubscriptions? ).to be_truthy
end
it 'can set message posting confirmation' do
expect( list.confirm_postings? ).to be_falsey
list.confirm_postings = true
expect( list.confirm_postings? ).to be_truthy
list.confirm_postings = false
expect( list.confirm_postings? ).to be_falsey
end
it 'can toggle remote subscriber lists for moderators' do
expect( list.allow_remote_listing? ).to be_falsey
list.allow_remote_listing = true
expect( list.allow_remote_listing? ).to be_truthy
list.allow_remote_listing = false
expect( list.allow_remote_listing? ).to be_falsey
end
it 'can toggle bounce management' do
expect( list.bounce_warnings? ).to be_truthy
list.bounce_warnings = false
expect( list.bounce_warnings? ).to be_falsey
list.bounce_warnings = true
expect( list.bounce_warnings? ).to be_truthy
end
it 'returns a default max message size' do
expect( list.maximum_message_size ).to eq( 0 )
end
it 'can set a new max message size' do
list.maximum_message_size = 1024 * 300
expect( list.maximum_message_size ).to eq( 307200 )
end
it 'can return the message count for a pristine list' do
expect( list.message_count ).to eq( 0 )
end
end
# it "can fetch the body of an archived post by message id"
# it "can fetch the header of an archived post by message id"
# it "can return a hash of the subjects of all archived posts to message ids"
# it "can return an Array of the subjects of all archived posts"
# it "can return a hash of the threads of all archived posts to message ids"
# it "can return an Array of the threads of all archived posts"
# it "can return a hash of the authors of all archived posts to message ids"
# it "can return an Array of the authors of all archived posts"

View file

@ -23,10 +23,13 @@ describe Ezmlm do
existant_mlentry = double( "mailinglist path that does exist", :exist? => true )
ml_dir_entry = double( "directory with a mailinglist file", :directory? => true, :+ => existant_mlentry )
sorted_dirs = double( "sorted dirs" )
expect( Pathname ).to receive( :glob ).with( an_instance_of(Pathname) ).
and_return( sorted_dirs )
expect( sorted_dirs ).to receive( :sort ).
and_return([ file_entry, nonml_dir_entry, ml_dir_entry ])
dirs = Ezmlm.find_directories( TEST_LISTSDIR )
dirs = Ezmlm.find_directories( '/tmp' )
expect( dirs.size ).to eq( 1 )
expect( dirs ).to include( ml_dir_entry )
@ -34,13 +37,13 @@ describe Ezmlm do
it "can iterate over all mailing lists in a specified directory" do
expect( Ezmlm ).to receive( :find_directories ).with( TEST_LISTSDIR ).and_return([ :listdir1, :listdir2 ])
expect( Ezmlm ).to receive( :find_directories ).with( '/tmp' ).and_return([ :listdir1, :listdir2 ])
expect( Ezmlm::List ).to receive( :new ).with( :listdir1 ).and_return( :listobject1 )
expect( Ezmlm::List ).to receive( :new ).with( :listdir2 ).and_return( :listobject2 )
lists = []
Ezmlm.each_list( TEST_LISTSDIR ) do |list|
Ezmlm.each_list( '/tmp' ) do |list|
lists << list
end

View file

@ -3,28 +3,41 @@
require 'simplecov' if ENV['COVERAGE']
require 'rspec'
require 'loggability/spechelpers'
require 'fileutils'
module SpecHelpers
include FileUtils
TEST_LISTSDIR = ENV['TEST_LISTSDIR'] || '/tmp/lists'
TEST_LIST_NAME = 'waffle-lovers'
TEST_LIST_HOST = 'lists.syrup.info'
TEST_OWNER = 'listowner@rumpus-the-whale.info'
TEST_SUBSCRIBERS = %w[
pete.chaffee@toadsmackers.com
dolphinzombie@alahalohamorra.com
piratebanker@yahoo.com
]
TEST_MODERATORS = %w[
dolphinzombie@alahalohamorra.com
]
###############
module_function
###############
### Create a temporary working directory and return
### a Pathname object for it.
### Create a copy of a fresh listdir into /tmp.
###
def make_tempdir
dirname = "%s.%d.%0.4f" % [
'ezmlm_spec',
def make_listdir
dirname = "/tmp/%s.%d.%0.4f" % [
'ezmlm_list',
Process.pid,
(Time.now.to_f % 3600),
]
tempdir = Pathname.new( Dir.tmpdir ) + dirname
tempdir.mkpath
list = Pathname.new( __FILE__ ).dirname + 'data' + 'testlist'
cp_r( list.to_s, dirname )
return tempdir
return dirname
end
end