diff --git a/.ruby-version b/.ruby-version index 6b4950e..95e3ba8 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.4 +2.5 diff --git a/README.md b/README.md index 80523ea..38a91a5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,13 @@ + # Ruby-Ezmlm -code -: https://bitbucket.org/mahlon/Ruby-Ezmlm + * home: + * http://code.martini.nu/ruby-ezmlm + * mirrors: + * https://github.com/mahlonsmith/ruby-ezmlm + * https://hg.sr.ht/~mahlon/ruby-ezmlm + * Ezmlm: + * http://untroubled.org/ezmlm/ ## Authors @@ -18,8 +24,6 @@ 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. *Strong recommendation*: Create your lists with archiving (-a) and @@ -46,7 +50,7 @@ be a generic interface for parsing and browsing list content. ## Limitations -This library doesn't create new lists from scratch. Use ezmlm-make. +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. diff --git a/Rakefile b/Rakefile index da4a692..537c19e 100644 --- a/Rakefile +++ b/Rakefile @@ -23,6 +23,7 @@ $version = ( LIBDIR + "#{PROJECT}.rb" ).read.split(/\n/). select{|line| line =~ /VERSION =/}.first.match(/([\d|.]+)/)[1] task :default => [ :spec, :docs, :package ] +task :spec => [ :compile ] ######################################################################## @@ -33,12 +34,8 @@ require 'rubygems' require 'rubygems/package_task' spec = Gem::Specification.new do |s| s.email = 'mahlon@martini.nu' - s.homepage = 'https://bitbucket.org/mahlon/Ruby-Ezmlm' - s.authors = [ - 'Mahlon E. Smith ', - 'Michael Granger ', - 'Jeremiah Jordan ' - ] + s.homepage = 'https://bitbucket.org/mahlon/ruby-ezmlm' + s.authors = [ 'Mahlon E. Smith', 'Michael Granger', 'Jeremiah Jordan' ] s.platform = Gem::Platform::RUBY s.summary = "Interact with Ezmlm-IDX mailing lists." s.name = PROJECT @@ -84,7 +81,7 @@ begin rdoc.rdoc_dir = 'docs' rdoc.main = "README.rdoc" # rdoc.options = [ '-f', 'fivefish' ] - rdoc.rdoc_files = [ 'lib', *FileList['ext/*/*.c'], *FileList['*.rdoc'] ] + rdoc.rdoc_files = [ 'lib', *FileList['ext/**.c'], *FileList['*.rdoc'], *FileList['*.md'] ] end RDoc::Task.new do |rdoc| diff --git a/USAGE.md b/USAGE.md index 5b5c9f7..6fc0059 100644 --- a/USAGE.md +++ b/USAGE.md @@ -10,14 +10,14 @@ Examples -------- -*Print the list address for all lists in a directory*: +### Print the list address for all lists in a directory: Ezmlm.each_list( '/lists' ) do |list| puts list.address end -*Check if I'm subscribed to a list, and if so, unsubscribe*: +### Check if I'm subscribed to a list, and if so, unsubscribe: (You don't really have to check first, subscribe and unsubscribe are idempotent.) @@ -31,14 +31,14 @@ idempotent.) puts "The list now has %d subscribers!" % [ list.subscribers.size ] -*Iterate over the subscriber list*: +### Iterate over the subscriber list: list.subscribers.each do |subscriber| # ... end -*Make the list moderated, and add a moderator*: +### Make the list moderated, and add a moderator: list.moderated = true list.add_moderator( 'mahlon@martini.nu' ) @@ -48,7 +48,7 @@ All other list behavior tunables operate in a similar fashion, see RDoc for details. -*Archiving!* +### Archiving! All of the archival pieces take advantage of Ezmlm-IDX extensions. If you want to use these features, you'll want to enable archiving @@ -67,12 +67,12 @@ rebuild the necessary files - afterwards, they are kept up to date automatically. -*How many messages are in the archive?*: +### How many messages are in the archive?: list.message_count #=> 123 -*Fetch message number 100 from the archive*: +### Fetch message number 100 from the archive: message = list.message( 100 ) or abort "No such message." @@ -96,7 +96,7 @@ Mikel Lindsaar (https://github.com/mikel/mail). See its documentation for specifics. -*Iterate over messages in a specific thread*: +### Iterate over messages in a specific thread: Messages know what thread they belong to. Once you have a thread object from a message, it is an enumerable. Iterate or sort on it using @@ -110,7 +110,7 @@ Threads are also aware of who participated in the conversation, via the 'authors' and 'each_author' methods. -*Iterate over messages from a specific author:* +### Iterate over messages from a specific author: Messages know who authored them. Once you have an author object from a message, it is an enumerable. Iterate or sort on it using standard Ruby diff --git a/ext/ezmlm/hash/hash.c b/ext/ezmlm/hash/hash.c index ff7cd94..3a31b2a 100644 --- a/ext/ezmlm/hash/hash.c +++ b/ext/ezmlm/hash/hash.c @@ -13,6 +13,7 @@ * */ +static void surf(unsigned int out[8],const unsigned int in[12],const unsigned int seed[32]) { unsigned int t[12]; unsigned int x; unsigned int sum = 0; @@ -32,6 +33,7 @@ void surf(unsigned int out[8],const unsigned int in[12],const unsigned int seed[ } } +static void surfpcs_init(surfpcs *s,const unsigned int k[32]) { int i; @@ -41,6 +43,7 @@ void surfpcs_init(surfpcs *s,const unsigned int k[32]) s->todo = 0; } +static void surfpcs_add(surfpcs *s,const char *x,unsigned int n) { int i; @@ -59,6 +62,7 @@ void surfpcs_add(surfpcs *s,const char *x,unsigned int n) } } +static void surfpcs_addlc(surfpcs *s,const char *x,unsigned int n) /* modified from surfpcs_add by case-independence and skipping ' ' & '\t' */ { @@ -84,6 +88,7 @@ void surfpcs_addlc(surfpcs *s,const char *x,unsigned int n) } } +static void surfpcs_out(surfpcs *s,unsigned char h[32]) { int i; @@ -95,6 +100,7 @@ void surfpcs_out(surfpcs *s,unsigned char h[32]) for (i = 0;i < 32;++i) h[i] = outdata[end[i]]; } +static void makehash(const char *indata,unsigned int inlen,char *hash) /* makes hash[COOKIE=20] from stralloc *indata, ignoring case and */ /* SPACE/TAB */ @@ -112,6 +118,7 @@ void makehash(const char *indata,unsigned int inlen,char *hash) hash[i] = 'a' + (h[i] & 15); } +static unsigned int subhashb(const char *s,long len) { unsigned long h; @@ -121,6 +128,7 @@ unsigned int subhashb(const char *s,long len) return h % 53; } +static unsigned int subhashs(const char *s) { return subhashb(s,strlen(s)); @@ -132,7 +140,7 @@ unsigned int subhashs(const char *s) /* - * call­seq: + * call-seq: * Ezmlm::Hash.address( email ) -> String * * Call the Surf hashing function on an +email+ address, returning @@ -141,7 +149,7 @@ unsigned int subhashs(const char *s) * the '<' character.) * */ -VALUE +static VALUE address( VALUE klass, VALUE email ) { char hash[20]; char *input; @@ -158,14 +166,14 @@ address( VALUE klass, VALUE email ) { /* - * call­seq: + * call-seq: * Ezmlm::Hash.subscriber( address ) -> String * * Call the subscriber hashing function on an email +address+, returning * the index character referring to the file containing subscriber presence. * */ -VALUE +static VALUE subscriber( VALUE klass, VALUE email ) { unsigned int prefix; diff --git a/ext/ezmlm/hash/hash.h b/ext/ezmlm/hash/hash.h index 9b4c15e..9200428 100644 --- a/ext/ezmlm/hash/hash.h +++ b/ext/ezmlm/hash/hash.h @@ -27,19 +27,19 @@ static const unsigned int littleendian[8] = { #define data ((unsigned char *) s->in) #define outdata ((unsigned char *) s->out) -extern void surf( unsigned int out[8], const unsigned int in[12], const unsigned int seed[32] ); -extern void surfpcs_init( surfpcs *s, const unsigned int k[32] ); -extern void surfpcs_add( surfpcs *s, const char *x,unsigned int n ); -extern void surfpcs_addlc( surfpcs *s, const char *x,unsigned int n ); -extern void surfpcs_out( surfpcs *s, unsigned char h[32] ); +static void surf( unsigned int out[8], const unsigned int in[12], const unsigned int seed[32] ); +static void surfpcs_init( surfpcs *s, const unsigned int k[32] ); +static void surfpcs_add( surfpcs *s, const char *x,unsigned int n ); +static void surfpcs_addlc( surfpcs *s, const char *x,unsigned int n ); +static void surfpcs_out( surfpcs *s, unsigned char h[32] ); #endif #ifndef SUBHASH_H #define SUBHASH_H -unsigned int subhashs(const char *s); -unsigned int subhashb(const char *s,long len); +static unsigned int subhashs(const char *s); +static unsigned int subhashb(const char *s,long len); #define subhashsa(SA) subhashb((SA)->s,(SA)->len) #endif diff --git a/lib/ezmlm.rb b/lib/ezmlm.rb index 9500728..1c2a911 100644 --- a/lib/ezmlm.rb +++ b/lib/ezmlm.rb @@ -1,5 +1,6 @@ # vim: set nosta noet ts=4 sw=4: +require 'pathname' # A Ruby interface to the ezmlm-idx mailing list system. # @@ -9,24 +10,11 @@ # puts "\"%s\" <%s>" % [ list.name, list.address ] # end # -# -# == Version -# -# $Id$ -# -#--- -# -# Please see the file LICENSE in the base directory for licensing details. -# - -require 'pathname' - - -### Toplevel namespace module module Ezmlm + # $Id$ # Package version - VERSION = '1.0.0' + VERSION = '1.1.2' # Suck in the components. # @@ -47,7 +35,7 @@ module Ezmlm def find_directories( listsdir ) listsdir = Pathname.new( listsdir ) return Pathname.glob( listsdir + '*' ).sort.select do |entry| - entry.directory? && ( entry + 'mailinglist' ).exist? + entry.directory? && ( entry + 'ezmlmrc' ).exist? end end diff --git a/lib/ezmlm/list.rb b/lib/ezmlm/list.rb index d88eec9..fc9814e 100644 --- a/lib/ezmlm/list.rb +++ b/lib/ezmlm/list.rb @@ -1,27 +1,19 @@ #!/usr/bin/ruby # vim: set nosta noet ts=4 sw=4: - -# A Ruby interface to a single Ezmlm-idx mailing list directory. -# -# list = Ezmlm::List.new( '/path/to/listdir' ) -# -# -# == Version -# -# $Id$ -# -#--- - require 'pathname' require 'time' require 'etc' require 'ezmlm' unless defined?( Ezmlm ) -### A Ruby interface to an ezmlm-idx mailing list directory -### +# A Ruby interface to a single Ezmlm-idx mailing list directory. +# +# list = Ezmlm::List.new( '/path/to/listdir' ) +# +#--- class Ezmlm::List + # $Id: list.rb,v a89d91d4b157 2017/06/23 17:54:26 mahlon $ # Valid subdirectories/sections for subscriptions. SUBSCRIPTION_DIRS = %w[ deny mod digest allow ] @@ -32,7 +24,7 @@ class Ezmlm::List ### def initialize( listdir ) listdir = Pathname.new( listdir ) unless listdir.is_a?( Pathname ) - unless listdir.directory? && ( listdir + 'mailinglist' ).exist? + unless listdir.directory? && ( listdir + 'ezmlmrc' ).exist? raise ArgumentError, "%p doesn't appear to be an ezmlm-idx list." % [ listdir.to_s ] end @listdir = listdir @@ -77,7 +69,7 @@ class Ezmlm::List ### Returns +true+ if +address+ is a subscriber to this list. ### def include?( addr, section: nil ) - addr.downcase! + addr = addr.downcase file = self.subscription_dir( section ) + Ezmlm::Hash.subscriber( addr ) return false unless file.exist? return file.read.scan( /T([^\0]+)\0/ ).flatten.include?( addr ) @@ -98,7 +90,7 @@ class Ezmlm::List def subscribe( *addr, section: nil ) addr.each do |address| next unless address.index( '@' ) - address.downcase! + address = address.downcase file = self.subscription_dir( section ) + Ezmlm::Hash.subscriber( address ) self.with_safety do @@ -124,7 +116,7 @@ class Ezmlm::List ### def unsubscribe( *addr, section: nil ) addr.each do |address| - address.downcase! + address = address.downcase file = self.subscription_dir( section ) + Ezmlm::Hash.subscriber( address ) self.with_safety do @@ -671,6 +663,16 @@ class Ezmlm::List end + ### Return a Time object for the last activity on the list, or nil + ### if archiving is disabled or there are no posts. + ### + def last_activity + file = self.listdir + 'archnum' + return unless file.exist? + return file.stat.mtime + end + + ### Parse all thread indexes into a single array that can be used ### as a lookup table. ### @@ -821,13 +823,11 @@ class Ezmlm::List ### def with_safety( &block ) home = self.homedir - mode = home.stat.mode - - home.chmod( mode | 01000 ) # enable sticky + home.chmod( home.stat.mode | 01000 ) # enable sticky yield ensure - home.chmod( mode ) + home.chmod( home.stat.mode & ~01000 ) # disable sticky end end # class Ezmlm::List diff --git a/lib/ezmlm/list/author.rb b/lib/ezmlm/list/author.rb index 561e517..d3257d2 100644 --- a/lib/ezmlm/list/author.rb +++ b/lib/ezmlm/list/author.rb @@ -1,6 +1,9 @@ #!/usr/bin/ruby # vim: set nosta noet ts=4 sw=4: +require 'pathname' +require 'ezmlm' unless defined?( Ezmlm ) + # A collection of messages authored from a unique user. # @@ -11,20 +14,9 @@ # author.name #=> "Help - navigate on interface?" # author.first.date.to_s #=> "2017-05-07T14:55:05-07:00" # -# -# == Version -# -# $Id$ -# #--- - -require 'pathname' -require 'ezmlm' unless defined?( Ezmlm ) - - -### A collection of messages for a specific author. -### class Ezmlm::List::Author + # $Id$ include Enumerable ### Instantiate a new list of messages given diff --git a/lib/ezmlm/list/message.rb b/lib/ezmlm/list/message.rb index 77e2d6e..cee6abf 100644 --- a/lib/ezmlm/list/message.rb +++ b/lib/ezmlm/list/message.rb @@ -1,6 +1,9 @@ #!/usr/bin/ruby # vim: set nosta noet ts=4 sw=4: +require 'pathname' +require 'ezmlm' unless defined?( Ezmlm ) +require 'mail' # An individual list message. # @@ -12,20 +15,9 @@ # This class passes all heavy lifting to the Mail::Message library. # Please see it for specifics on usage. # -# == Version -# -# $Id$ -# #--- - -require 'pathname' -require 'ezmlm' unless defined?( Ezmlm ) -require 'mail' - - -### A Ruby interface to an individual list message. -### class Ezmlm::List::Message + # $Id$ ### Instantiate a new messag from a +list+ and a +message_number+. ### diff --git a/lib/ezmlm/list/thread.rb b/lib/ezmlm/list/thread.rb index e154a61..79576ad 100644 --- a/lib/ezmlm/list/thread.rb +++ b/lib/ezmlm/list/thread.rb @@ -1,6 +1,9 @@ #!/usr/bin/ruby # vim: set nosta noet ts=4 sw=4: +require 'pathname' +require 'ezmlm' unless defined?( Ezmlm ) + # A collection of messages for a specific archive thread. # @@ -8,20 +11,9 @@ # thread.subject #=> "Help - navigate on interface?" # thread.first.date.to_s #=> "2017-05-07T14:55:05-07:00" # -# -# == Version -# -# $Id$ -# #--- - -require 'pathname' -require 'ezmlm' unless defined?( Ezmlm ) - - -### A collection of messages for a specific archive thread. -### class Ezmlm::List::Thread + # $Id$ include Enumerable ### Instantiate a new thread of messages given diff --git a/spec/data/testlist/allow/subscribers/.placeholder b/spec/data/testlist/allow/subscribers/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/spec/data/testlist/bounce/.placeholder b/spec/data/testlist/bounce/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/spec/data/testlist/deny/subscribers/.placeholder b/spec/data/testlist/deny/subscribers/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/spec/data/testlist/digest/bounce/.placeholder b/spec/data/testlist/digest/bounce/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/spec/data/testlist/digest/subscribers/.placeholder b/spec/data/testlist/digest/subscribers/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/spec/data/testlist/mod/accepted/.placeholder b/spec/data/testlist/mod/accepted/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/spec/data/testlist/mod/pending/.placeholder b/spec/data/testlist/mod/pending/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/spec/data/testlist/mod/rejected/.placeholder b/spec/data/testlist/mod/rejected/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/spec/data/testlist/mod/subscribers/.placeholder b/spec/data/testlist/mod/subscribers/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/spec/data/testlist/mod/unconfirmed/.placeholder b/spec/data/testlist/mod/unconfirmed/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/spec/data/testlist/subscribers/.placeholder b/spec/data/testlist/subscribers/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/spec/data/testlist/text/.placeholder b/spec/data/testlist/text/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/spec/ezmlm/list_spec.rb b/spec/ezmlm/list_spec.rb index 08d3619..3f0d4b4 100644 --- a/spec/ezmlm/list_spec.rb +++ b/spec/ezmlm/list_spec.rb @@ -236,6 +236,10 @@ describe Ezmlm::List do expect( list.digest_count ).to eq( 10 ) end + it 'knows the date of the most recent posting' do + expect( list.last_activity ).to be_a( Time ) + end + it 'can set a new digest message count' do list.digest_count = 25 expect( list.digest_count ).to eq( 25 ) diff --git a/spec/spec_helpers.rb b/spec/spec_helpers.rb index 6fe76e4..13db8b7 100644 --- a/spec/spec_helpers.rb +++ b/spec/spec_helpers.rb @@ -3,6 +3,7 @@ require 'simplecov' if ENV['COVERAGE'] require 'rspec' require 'fileutils' +require 'tmpdir' require_relative '../lib/ezmlm' @@ -30,7 +31,8 @@ module SpecHelpers ### Create a copy of a fresh listdir into /tmp. ### def make_listdir - dirname = "/tmp/%s.%d.%0.4f" % [ + dirname = "%s/%s.%d.%0.4f" % [ + Dir.tmpdir, 'ezmlm_list', Process.pid, (Time.now.to_f % 3600),