Multiple changes.

- Remove the runtime dependency on rake-compiler.
 - Use #rb_str_new instead of #rb_str_new2, the hash character array
   isn't null terminated.
 - Add various safeguards for object instantiations.
 - Remove the 'threaded' options for messages, folding them into
   'archived'.  If archiving is enabled, so is threading.
 - Return nil for lookups from the list object instead of raising
   exceptions.
 - Open subject indexes with the proper encodings (thanks Michael
   Granger!)
 - Allow touching and unlinking files to operate on multiple paths
   at once, within a single safety() wrap.
This commit is contained in:
Mahlon E. Smith 2017-05-16 13:58:34 -07:00
parent c99bdfe747
commit 3871084daa
17 changed files with 257 additions and 110 deletions

View file

@ -1,8 +1,12 @@
#!/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$
@ -28,6 +32,9 @@ class Ezmlm::List
###
def initialize( listdir )
listdir = Pathname.new( listdir ) unless listdir.is_a?( Pathname )
unless listdir.directory? && ( listdir + 'mailinglist' ).exist?
raise ArgumentError, "%p doesn't appear to be an ezmlm-idx list." % [ listdir.to_s ]
end
@listdir = listdir
end
@ -219,27 +226,6 @@ class Ezmlm::List
end
### 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
alias_method :threaded, :threaded=
### Returns +true+ if the list is configured to respond
### to remote management requests.
###
@ -380,21 +366,23 @@ class Ezmlm::List
### Returns +true+ if message archival is enabled.
###
def archived?
return ( self.listdir + 'archived' ).exist? || ( self.listdir + 'indexed' ).exist?
test = %w[ archived indexed threaded ].each_with_object( [] ) do |f, acc|
acc << self.listdir + f
end
return test.all?( &:exist? )
end
### Disable or enable message archiving (and indexing.)
### Disable or enable message archiving (and indexing/threading.)
###
def archive=( enable=true )
def archived=( enable=true )
if enable
self.touch( 'archived' )
self.touch( 'indexed' )
self.touch( 'archived', 'indexed', 'threaded' )
else
self.unlink( 'archived' )
self.unlink( 'indexed' )
self.unlink( 'archived', 'indexed', 'threaded' )
end
end
alias_method :archive, :archive=
alias_method :archived, :archived=
### Returns +true+ if the message archive is accessible only to
### moderators.
@ -653,9 +641,8 @@ class Ezmlm::List
### Returns an individual message if archiving was enabled.
###
def message( message_id )
raise "Archiving is not enabled." unless self.archived?
raise "Message archive is empty." if self.message_count.zero?
return Ezmlm::List::Message.new( self, message_id )
return Ezmlm::List::Message.new( self, message_id ) rescue nil
end
### Lazy load each message ID as a Ezmlm::List::Message,
@ -671,8 +658,7 @@ class Ezmlm::List
### Return a Thread object for the given +thread_id+.
###
def thread( thread_id )
raise "Archiving is not enabled." unless self.archived?
return Ezmlm::List::Thread.new( self, thread_id )
return Ezmlm::List::Thread.new( self, thread_id ) rescue nil
end
@ -680,9 +666,8 @@ class Ezmlm::List
### could also be an email address.
###
def author( author_id )
raise "Archiving is not enabled." unless self.archived?
author_id = Ezmlm::Hash.address(author_id) if author_id.index( '@' )
return Ezmlm::List::Author.new( self, author_id )
return Ezmlm::List::Author.new( self, author_id ) rescue nil
end
@ -700,22 +685,25 @@ class Ezmlm::List
index = archivedir + dir.to_s + 'index'
next unless index.exist?
index.each_line.lazy.slice_before( /^\d+:/ ).each do |message|
match = message[0].match( /^(?<message_id>\d+): (?<thread_id>\w+)/ )
next unless match
thread_id = match[ :thread_id ]
index.open( 'r', encoding: Encoding::ISO8859_1 ) do |fh|
fh.each_line.lazy.slice_before( /^\d+:/ ).each do |message|
match = message[1].match( /^(?<date>[^;]+);(?<author_id>\w+) / )
next unless match
author_id = match[ :author_id ]
date = match[ :date ]
match = message[0].match( /^(?<message_id>\d+): (?<thread_id>\w+)/ )
next unless match
thread_id = match[ :thread_id ]
metadata = {
date: Time.parse( date ),
thread: thread_id,
author: author_id
}
acc << metadata
match = message[1].match( /^(?<date>[^;]+);(?<author_id>\w+) / )
next unless match
author_id = match[ :author_id ]
date = match[ :date ]
metadata = {
date: Time.parse( date ),
thread: thread_id,
author: author_id
}
acc << metadata
end
end
end
@ -757,18 +745,25 @@ class Ezmlm::List
### Simply create an empty file, safely.
###
def touch( file )
self.write( file ) {}
def touch( *file )
self.with_safety do
Array( file ).flatten.each do |f|
f = self.listdir + f unless f.is_a?( Pathname )
f.open( 'w' ) {}
end
end
end
### Delete +file+ safely.
###
def unlink( file )
file = self.listdir + file unless file.is_a?( Pathname )
return unless file.exist?
def unlink( *file )
self.with_safety do
file.unlink
Array( file ).flatten.each do |f|
f = self.listdir + f unless f.is_a?( Pathname )
next unless f.exist?
f.unlink
end
end
end

View file

@ -1,6 +1,7 @@
#!/usr/bin/ruby
# vim: set nosta noet ts=4 sw=4:
#
# A collection of messages authored from a unique user.
#
# Note that Ezmlm uses the "real name" part of an address
@ -27,12 +28,12 @@ class Ezmlm::List::Author
include Enumerable
### Instantiate a new list of messages given
### a +list+ and a +author_id+.
### a +list+ and an +author_id+.
###
def initialize( list, author_id )
raise ArgumentError, "Unknown list object." unless list.respond_to?( :listdir )
raise ArgumentError, "Malformed Author ID." unless author_id =~ /^\w{20}$/
raise "Thread indexing is not enabled." unless list.threaded?
raise "Archiving is not enabled." unless list.archived?
@list = list
@id = author_id
@ -67,6 +68,7 @@ class Ezmlm::List::Author
yield Ezmlm::List::Message.new( self.list, id )
end
end
alias_method :each_message, :each
### Lazy load each thread ID as a Ezmlm::List::Thread, yielding it to the block.
@ -92,13 +94,15 @@ class Ezmlm::List::Author
path = self.author_path
raise "Unknown author: %p" % [ self.id ] unless path.exist?
path.each_line.with_index do |line, i|
if i.zero?
@name = line.match( /^\w+ (.+)/ )[1]
else
match = line.match( /^(\d+):\d+:(\w+) / ) or next
self.messages << match[1].to_i
self.threads << match[2]
path.open( 'r', encoding: Encoding::ISO8859_1 ) do |fh|
fh.each_line.with_index do |line, i|
if i.zero?
@name = line.match( /^\w+ (.+)/ )[1]
else
match = line.match( /^(\d+):\d+:(\w+) / ) or next
self.messages << match[1].to_i
self.threads << match[2]
end
end
end
end

View file

@ -1,6 +1,7 @@
#!/usr/bin/ruby
# vim: set nosta noet ts=4 sw=4:
#
# An individual list message.
#
# message = Ezmlm::List::Message.new( list, 24 )
@ -31,6 +32,7 @@ class Ezmlm::List::Message
def initialize( list, message_number=0 )
raise ArgumentError, "Unknown list object." unless list.respond_to?( :listdir )
raise ArgumentError, "Invalid message number (impossible)" if message_number < 1
raise "Archiving is not enabled." unless list.archived?
raise ArgumentError, "Invalid message number (out of list bounds)" if message_number > list.message_count
@list = list

View file

@ -1,6 +1,7 @@
#!/usr/bin/ruby
# vim: set nosta noet ts=4 sw=4:
#
# A collection of messages for a specific archive thread.
#
# thread = Ezmlm::List::Thread.new( list, 'acgcbmbmeapgpfckcdol' )
@ -29,7 +30,7 @@ class Ezmlm::List::Thread
def initialize( list, thread_id )
raise ArgumentError, "Unknown list object." unless list.respond_to?( :listdir )
raise ArgumentError, "Malformed Thread ID." unless thread_id =~ /^\w{20}$/
raise "Thread indexing is not enabled." unless list.threaded?
raise "Archiving is not enabled." unless list.archived?
@list = list
@id = thread_id
@ -65,6 +66,18 @@ class Ezmlm::List::Thread
yield Ezmlm::List::Message.new( self.list, id )
end
end
alias_method :each_message, :each
### Lazy load each author ID as a Ezmlm::List::Author, yielding it
### to the block.
###
def each_author
self.load_thread # refresh for any thread updates since object was created
self.authors.each do |id|
yield Ezmlm::List::Author.new( self.list, id )
end
end
#########
@ -79,13 +92,15 @@ class Ezmlm::List::Thread
path = self.thread_path
raise "Unknown thread: %p" % [ self.id ] unless path.exist?
path.each_line.with_index do |line, i|
if i.zero?
@subject = line.match( /^\w+ (.+)/ )[1]
else
match = line.match( /^(\d+):\d+:(\w+) / ) or next
self.messages << match[1].to_i
self.authors << match[2]
path.open( 'r', encoding: Encoding::ISO8859_1 ) do |fh|
fh.each_line.with_index do |line, i|
if i.zero?
@subject = line.match( /^\w+ (.+)/ )[1]
else
match = line.match( /^(\d+):\d+:(\w+) / ) or next
self.messages << match[1].to_i
self.authors << match[2]
end
end
end
end