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.
--- a/.hgignore Fri May 12 16:17:41 2017 -0700
+++ b/.hgignore Tue May 16 13:58:34 2017 -0700
@@ -2,3 +2,4 @@
pkg/*
docs/*
tmp/*
+lib/ezmlm/hash.so
--- a/README.md Fri May 12 16:17:41 2017 -0700
+++ b/README.md Tue May 16 13:58:34 2017 -0700
@@ -37,10 +37,6 @@
$ gem install ezmlm
-## Usage
-
- ....
-
## TODO
--- a/Rakefile Fri May 12 16:17:41 2017 -0700
+++ b/Rakefile Tue May 16 13:58:34 2017 -0700
@@ -59,7 +59,7 @@
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|
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/USAGE.md Tue May 16 13:58:34 2017 -0700
@@ -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.
+
+
--- a/ext/ezmlm/hash/hash.c Fri May 12 16:17:41 2017 -0700
+++ b/ext/ezmlm/hash/hash.c Tue May 16 13:58:34 2017 -0700
@@ -148,12 +148,12 @@
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 );
}
--- a/lib/ezmlm.rb Fri May 12 16:17:41 2017 -0700
+++ b/lib/ezmlm.rb Tue May 16 13:58:34 2017 -0700
@@ -1,7 +1,14 @@
-#!/usr/bin/ruby
# vim: set nosta noet ts=4 sw=4:
+
+
+# A Ruby interface to the ezmlm-idx mailing list system.
#
-# 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
#
--- a/lib/ezmlm/list.rb Fri May 12 16:17:41 2017 -0700
+++ b/lib/ezmlm/list.rb Tue May 16 13:58:34 2017 -0700
@@ -1,7 +1,11 @@
#!/usr/bin/ruby
# vim: set nosta noet ts=4 sw=4:
+
+
+# A Ruby interface to a single Ezmlm-idx mailing list directory.
#
-# A Ruby interface to a single Ezmlm-idx mailing list directory.
+# list = Ezmlm::List.new( '/path/to/listdir' )
+#
#
# == Version
#
@@ -28,6 +32,9 @@
###
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 @@
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 @@
### 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 @@
### 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 @@
### 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 @@
### 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 @@
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[0].match( /^(?<message_id>\d+): (?<thread_id>\w+)/ )
+ next unless match
+ thread_id = match[ :thread_id ]
- match = message[1].match( /^(?<date>[^;]+);(?<author_id>\w+) / )
- next unless match
- author_id = match[ :author_id ]
- date = match[ :date ]
+ 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
+ metadata = {
+ date: Time.parse( date ),
+ thread: thread_id,
+ author: author_id
+ }
+ acc << metadata
+ end
end
end
@@ -757,18 +745,25 @@
### 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
--- a/lib/ezmlm/list/author.rb Fri May 12 16:17:41 2017 -0700
+++ b/lib/ezmlm/list/author.rb Tue May 16 13:58:34 2017 -0700
@@ -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 @@
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 @@
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 @@
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
--- a/lib/ezmlm/list/message.rb Fri May 12 16:17:41 2017 -0700
+++ b/lib/ezmlm/list/message.rb Tue May 16 13:58:34 2017 -0700
@@ -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 @@
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
--- a/lib/ezmlm/list/thread.rb Fri May 12 16:17:41 2017 -0700
+++ b/lib/ezmlm/list/thread.rb Tue May 16 13:58:34 2017 -0700
@@ -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 @@
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 @@
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 @@
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
--- a/spec/data/testlist/archive/0/index Fri May 12 16:17:41 2017 -0700
+++ b/spec/data/testlist/archive/0/index Tue May 16 13:58:34 2017 -0700
@@ -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 SCSIs 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/spec/data/testlist/mailinglist Tue May 16 13:58:34 2017 -0700
@@ -0,0 +1,1 @@
+contact testlist-help@lists.laika.com; run by ezmlm
--- a/spec/ezmlm/list/author_spec.rb Fri May 12 16:17:41 2017 -0700
+++ b/spec/ezmlm/list/author_spec.rb Tue May 16 13:58:34 2017 -0700
@@ -28,11 +28,11 @@
}.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 @@
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 )
--- a/spec/ezmlm/list/message_spec.rb Fri May 12 16:17:41 2017 -0700
+++ b/spec/ezmlm/list/message_spec.rb Tue May 16 13:58:34 2017 -0700
@@ -44,6 +44,7 @@
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 )
--- a/spec/ezmlm/list/thread_spec.rb Fri May 12 16:17:41 2017 -0700
+++ b/spec/ezmlm/list/thread_spec.rb Tue May 16 13:58:34 2017 -0700
@@ -29,10 +29,10 @@
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 @@
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 )
--- a/spec/ezmlm/list_spec.rb Fri May 12 16:17:41 2017 -0700
+++ b/spec/ezmlm/list_spec.rb Tue May 16 13:58:34 2017 -0700
@@ -113,16 +113,6 @@
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 @@
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 @@
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 @@
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
--- a/spec/spec_helpers.rb Fri May 12 16:17:41 2017 -0700
+++ b/spec/spec_helpers.rb Tue May 16 13:58:34 2017 -0700
@@ -2,7 +2,6 @@
require 'simplecov' if ENV['COVERAGE']
require 'rspec'
-require 'loggability/spechelpers'
require 'fileutils'
require_relative '../lib/ezmlm'