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

@ -2,3 +2,4 @@ Session.vim
pkg/*
docs/*
tmp/*
lib/ezmlm/hash.so

View file

@ -37,10 +37,6 @@ be a generic interface for parsing and browsing list content.
$ gem install ezmlm
## Usage
....
## TODO

View file

@ -59,7 +59,7 @@ environment.)
s.required_ruby_version = '>= 2.1'
s.add_dependency 'mail', "~> 2.6"
s.add_dependency 'rake-compiler', "~> 1.0"
s.add_development_dependency 'rake-compiler', "~> 1.0"
end
Gem::PackageTask.new( spec ) do |pkg|

126
USAGE.md Normal file
View file

@ -0,0 +1,126 @@
Usage
=======
Here's a quick rundown of how to use this library. For specifics, see
the generated RDoc.
Examples
--------
*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*:
(You don't really have to check first, subscribe and unsubscribe are
idempotent.)
list = Ezmlm::List.new( '/lists/waffle-lovers' )
if list.include?( 'mahlon@martini.nu' )
list.unsubscribe( 'mahlon@martini.nu' )
end
puts "The list now has %d subscribers!" % [ list.subscribers.size ]
*Iterate over the subscriber list*:
list.subscribers.each do |subscriber|
# ...
end
*Make the list moderated, and add a moderator*:
list.moderated = true
list.add_moderator( 'mahlon@martini.nu' )
list.moderated? #=> true
All other list behavior tunables operate in a similar fashion, see RDoc
for details.
*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
and indexing for your lists, using the -a and -i flags to ezmlm-make.
(Enabling archiving with this library also enables indexing and thread
indexes, I assume that since you're using ezmlm-idx, you want these
enhancements!)
list.archived? #=> false
list.archived = true
list.archived? #=> true
If your list(s) already had archiving enabled (the default to
ezmlm-make) but not indexing, you can manually run ezmlm-archive to
rebuild the necessary files - afterwards, they are kept up to date
automatically.
*How many messages are in the archive?*:
list.message_count #=> 123
*Fetch message number 100 from the archive*:
message = list.message( 100 ) or abort "No such message."
puts message.subject
puts message.body.to_s # Print just the body of the message.
puts message.to_s # Print the entire, unparsed message.
thread = message.thread # Returns an Ezmlm::List::Thread object
author = message.author # Returns an Ezmlm::List::Author object
As a general rule, methods called on the Ezmlm::List object return nil
if they are unable to perform the requested task. Instantiating the
underlying objects directly raise with a specific error. The following
are equivalent, but behave differently:
message = list.message( 10000 ) # nonexistent message, returns nil
message = Ezmlm::List::Message.new( list, 10000 ) # Raises a RuntimeError
Message objects act as "Mail" objects from the excellent library from
Mikel Lindsaar (https://github.com/mikel/mail). See its documentation
for specifics.
*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
standard Ruby methods.
thread.each do |message|
# ...
end
Threads are also aware of who participated in the conversation, via the
'authors' and 'each_author' methods.
*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
methods.
author.each do |message|
# ...
end
An Author object is also aware of all threads the author participated
in, via the 'threads' and 'each_thread' methods.

View file

@ -148,12 +148,12 @@ address( VALUE klass, VALUE email ) {
Check_Type( email, T_STRING );
email = rb_str_plus( rb_str_new2("<"), email);
email = rb_str_plus( rb_str_new2("<"), email );
input = StringValueCStr( email );
makehash( input, strlen(input), hash );
return rb_str_new2( hash );
return rb_str_new( hash, 20 );
}

View file

@ -1,8 +1,15 @@
#!/usr/bin/ruby
# vim: set nosta noet ts=4 sw=4:
#
# A Ruby interface to the ezmlm-idx mailing list system.
#
# Ezmlm.find_directories( '/lists' ) #=> [ Ezmlm::List, Ezmlm::List ]
#
# Ezmlm.each_list( '/lists' ) do |list|
# puts "\"%s\" <%s>" % [ list.name, list.address ]
# end
#
#
# == Version
#
# $Id$

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
### Disable or enable message archiving (and indexing.)
return test.all?( &:exist? )
end
### 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,7 +685,9 @@ class Ezmlm::List
index = archivedir + dir.to_s + 'index'
next unless index.exist?
index.each_line.lazy.slice_before( /^\d+:/ ).each do |message|
index.open( 'r', encoding: Encoding::ISO8859_1 ) do |fh|
fh.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 ]
@ -718,6 +705,7 @@ class Ezmlm::List
acc << metadata
end
end
end
return idx
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,7 +94,8 @@ 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|
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
@ -102,6 +105,7 @@ class Ezmlm::List::Author
end
end
end
end
### Return the path on disk for the author index.

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,7 +92,8 @@ 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|
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
@ -89,6 +103,7 @@ class Ezmlm::List::Thread
end
end
end
end
### Return the path on disk for the thread index.

View file

@ -20,7 +20,7 @@
7 May 2017 21:55:05 -0000;gclofnmfhpbehngoppdg Amalia Purdy
11: bhfejoliggjidmkbclnn Parsing panel for a hard drive?????
7 May 2017 21:55:05 -0000;decddajmifhkgodaginh Sally Pagac
12: fbhfcpngckkjbhlfjooh Trying to override SCSI on the microchip.
12: fbhfcpngckkjbhlfjooh Trying to override SCSI’s on the microchip.
7 May 2017 21:55:05 -0000;kdlcjeacpilheebkcbaf Wayne Friesen
13: mipieokohgoiigideadf Generating circuit for a application?????
7 May 2017 21:55:05 -0000;ojjhjlapnejjlbcplabi Jena Smitham

View file

@ -0,0 +1 @@
contact testlist-help@lists.laika.com; run by ezmlm

View file

@ -28,11 +28,11 @@ describe Ezmlm::List::Author do
}.to raise_error( ArgumentError, /unknown list/i )
end
it 'raises error if thread indexing is disabled' do
expect( list ).to receive( :threaded? ).and_return( false )
it 'raises error if thread archiving is disabled' do
expect( list ).to receive( :archived? ).and_return( false )
expect {
described_class.new( list, author_id )
}.to raise_error( RuntimeError, /indexing is not enabled/i )
}.to raise_error( RuntimeError, /archiving is not enabled/i )
end
it 'raises error if passed a malformed author ID' do
@ -43,7 +43,7 @@ describe Ezmlm::List::Author do
it 'raises error when unable to read index file' do
allow( list ).to receive( :listdir ).and_return( Pathname('/nope') )
expect( list ).to receive( :threaded? ).and_return( true )
expect( list ).to receive( :archived? ).and_return( true )
expect {
described_class.new( list, author_id )
}.to raise_error( RuntimeError, /unknown author/i )

View file

@ -44,6 +44,7 @@ describe Ezmlm::List::Message do
it 'raises error when unable to read message' do
allow( list ).to receive( :listdir ).and_return( Pathname('/nope') )
expect( list ).to receive( :message_count ).and_return( 1 )
expect( list ).to receive( :archived? ).and_return( true )
expect {
described_class.new( list, 1 )
}.to raise_error( RuntimeError, /unable to determine message path/i )

View file

@ -29,10 +29,10 @@ describe Ezmlm::List::Thread do
end
it 'raises error if thread indexing is disabled' do
expect( list ).to receive( :threaded? ).and_return( false )
expect( list ).to receive( :archived? ).and_return( false )
expect {
described_class.new( list, thread_id )
}.to raise_error( RuntimeError, /indexing is not enabled/i )
}.to raise_error( RuntimeError, /archiving is not enabled/i )
end
it 'raises error if passed a malformed thread ID' do
@ -43,7 +43,7 @@ describe Ezmlm::List::Thread do
it 'raises error when unable to read thread file' do
allow( list ).to receive( :listdir ).and_return( Pathname('/nope') )
expect( list ).to receive( :threaded? ).and_return( true )
expect( list ).to receive( :archived? ).and_return( true )
expect {
described_class.new( list, thread_id )
}.to raise_error( RuntimeError, /unknown thread/i )

View file

@ -113,16 +113,6 @@ describe Ezmlm::List do
end
it 'returns the current threading state' do
expect( list.threaded? ).to be_truthy
end
it 'can set the threading state' do
list.threaded = false
expect( list.threaded? ).to be_falsey
end
it 'returns the current public/private state' do
expect( list.public? ).to be_truthy
expect( list.private? ).to be_falsey
@ -200,9 +190,9 @@ describe Ezmlm::List do
it 'can set archival status' do
expect( list.archived? ).to be_truthy
list.archive = false
list.archived = false
expect( list.archived? ).to be_falsey
list.archive = true
list.archived = true
expect( list.archived? ).to be_truthy
end
@ -336,6 +326,10 @@ describe Ezmlm::List do
expect( list.thread('cadgeokhhaieijmndokb') ).to be_a( Ezmlm::List::Thread )
end
it 'returns nil when fetching an invalid thread' do
expect( list.thread('whatever') ).to be_nil
end
it 'fetches author objects upon request' do
expect( list.author('ojjhjlapnejjlbcplabi') ).to be_a( Ezmlm::List::Author )
@ -346,13 +340,19 @@ describe Ezmlm::List do
expect( list.author('yvette@example.net').name ).to eq( author.name )
end
it 'returns nil when fetching an invalid author' do
expect( list.author('whatever') ).to be_nil
end
context 'fetching messages' do
it 'raises an error if archiving is disabled' do
it 'returns nil if archiving is disabled' do
expect( list ).to receive( :archived? ).and_return( false )
expect {
list.message( 1 )
}.to raise_error( RuntimeError, /archiving is not enabled/i )
expect( list.message(1) ).to be_nil
end
it 'returns nil when fetching an invalid message id' do
expect( list.message(2389234) ).to be_nil
end
it 'raises an error if the message archive is empty' do

View file

@ -2,7 +2,6 @@
require 'simplecov' if ENV['COVERAGE']
require 'rspec'
require 'loggability/spechelpers'
require 'fileutils'
require_relative '../lib/ezmlm'