--- a/lib/ezmlm/list.rb Mon Feb 06 11:54:16 2017 -0800
+++ b/lib/ezmlm/list.rb Fri May 12 11:09:36 2017 -0700
@@ -10,9 +10,9 @@
#---
require 'pathname'
+require 'time'
require 'etc'
-require 'ezmlm'
-require 'mail'
+require 'ezmlm' unless defined?( Ezmlm )
### A Ruby interface to an ezmlm-idx mailing list directory
@@ -21,13 +21,7 @@
# Quick address space detection, to (hopefully)
# match the overflow size on this machine.
- #
- ADDRESS_SPACE = case [ 'i' ].pack( 'p' ).size
- when 4
- 32
- when 8
- 64
- end
+ ADDRESS_SPACE = [ 'i' ].pack( 'p' ).size * 8
# Valid subdirectories/sections for subscriptions.
SUBSCRIPTION_DIRS = %w[ deny mod digest allow ]
@@ -247,10 +241,11 @@
self.unlink( 'threaded' )
end
end
+ alias_method :threaded, :threaded=
### Returns +true+ if the list is configured to respond
- ### to remote mangement requests.
+ ### to remote management requests.
###
def public?
return ( self.listdir + 'public' ).exist?
@@ -265,9 +260,10 @@
self.unlink( 'public' )
end
end
+ alias_method :public, :public=
### Returns +true+ if the list is not configured to respond
- ### to remote mangement requests.
+ ### to remote management requests.
###
def private?
return ! self.public?
@@ -278,6 +274,7 @@
def private=( enable=false )
self.public = ! enable
end
+ alias_method :private, :private=
### Returns +true+ if the list supports remote administration
@@ -296,6 +293,7 @@
self.unlink( 'remote' )
end
end
+ alias_method :remote_subscriptions, :remote_subscriptions=
### Returns +true+ if list subscription requests require moderator
@@ -314,7 +312,7 @@
self.unlink( 'modsub' )
end
end
-
+ alias_method :moderated_subscriptions, :moderated_subscriptions=
### Returns +true+ if message moderation is enabled.
###
@@ -324,7 +322,7 @@
### Disable or enable message moderation.
###
- ### This has special meaning when combined with user_post_only setting.
+ ### This has special meaning when combined with user_posts_only setting.
### Lists act as unmoderated for subscribers, and posts from unknown
### addresses go to moderation.
###
@@ -337,6 +335,7 @@
self.unlink( 'noreturnposts' ) if self.user_posts_only?
end
end
+ alias_method :moderated, :moderated=
### Returns +true+ if posting is only allowed by moderators.
@@ -354,6 +353,7 @@
self.unlink( 'modpostonly' )
end
end
+ alias_method :moderator_posts_only, :moderator_posts_only=
### Returns +true+ if posting is only allowed by subscribers.
@@ -378,7 +378,7 @@
self.unlink( 'noreturnposts' ) if self.moderated?
end
end
-
+ alias_method :user_posts_only, :user_posts_only=
### Returns +true+ if message archival is enabled.
@@ -398,6 +398,7 @@
self.unlink( 'indexed' )
end
end
+ alias_method :archive, :archive=
### Returns +true+ if the message archive is accessible only to
### moderators.
@@ -415,6 +416,7 @@
self.unlink( 'modgetonly' )
end
end
+ alias_method :private_archive, :private_archive=
### Returns +true+ if the message archive is accessible to anyone.
###
@@ -422,6 +424,13 @@
return ! self.private_archive?
end
+ ### Disable or enable private access to the archive.
+ ###
+ def public_archive=( enable=true )
+ self.private_archive = ! enable
+ end
+ alias_method :public_archive, :public_archive=
+
### Returns +true+ if the message archive is accessible only to
### list subscribers.
###
@@ -438,6 +447,7 @@
self.unlink( 'subgetonly' )
end
end
+ alias_method :guarded_archive, :guarded_archive=
### Returns +true+ if message digests are enabled.
@@ -455,6 +465,7 @@
self.unlink( 'digested' )
end
end
+ alias_method :digest, :digest=
### If the list is digestable, trigger the digest after this amount
### of message body since the latest digest, in kbytes.
@@ -531,6 +542,7 @@
self.touch( 'nosubconfirm' )
end
end
+ alias_method :confirm_subscriptions, :confirm_subscriptions=
### Returns +true+ if the list requires unsubscriptions to be
### confirmed. AKA "jump" mode.
@@ -549,6 +561,7 @@
self.touch( 'nounsubconfirm' )
end
end
+ alias_method :confirm_unsubscriptions, :confirm_unsubscriptions=
### Returns +true+ if the list requires regular message postings
@@ -567,6 +580,7 @@
self.unlink( 'confirmpost' )
end
end
+ alias_method :confirm_postings, :confirm_postings=
### Returns +true+ if the list allows moderators to
@@ -586,6 +600,7 @@
self.unlink( 'modcanlist' )
end
end
+ alias_method :allow_remote_listing, :allow_remote_listing=
### Returns +true+ if the list automatically manages
@@ -604,6 +619,7 @@
self.touch( 'nowarn' )
end
end
+ alias_method :bounce_warnings, :bounce_warnings=
### Return the maximum message size, in bytes. Messages larger than
@@ -617,7 +633,7 @@
end
### Set the maximum message size, in bytes. Messages larger than
- ### this size will be rejected.
+ ### this size will be rejected. Defaults to 300kb.
###
### See: ezmlm-reject(1)
###
@@ -630,6 +646,7 @@
end
+
### Return the number of messages in the list archive.
###
def message_count
@@ -637,20 +654,74 @@
return count ? Integer( count ) : 0
end
- ### Returns the last message to the list as a Mail::Message, if
- ### archiving was enabled.
+ ### 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 )
+ end
+
+ ### Lazy load each message ID as a Ezmlm::List::Message,
+ ### yielding it to the block.
###
- def last_post
- num = self.message_count
- return if num.zero?
+ def each_message
+ ( 1 .. self.message_count ).each do |id|
+ yield self.message( id )
+ end
+ end
+
+
+ ### 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 )
+ end
+
+
+ ### Return an Author object for the given +author_id+.
+ ###
+ def author( author_id )
+ raise "Archiving is not enabled." unless self.archived?
+ return Ezmlm::List::Author.new( self, author_id )
+ end
+
- hashdir = num / 100
- message = "%02d" % [ num % 100 ]
+ ### Parse all thread indexes into a single array that can be used
+ ### as a lookup table.
+ ###
+ ### These are not expanded into objects, use #message, #thread,
+ ### and #author to do so.
+ ###
+ def index
+ raise "Archiving is not enabled." unless self.archived?
+ archivedir = listdir + 'archive'
+
+ idx = ( 0 .. self.message_count / 100 ).each_with_object( [] ) do |dir, acc|
+ index = archivedir + dir.to_s + 'index'
+ next unless index.exist?
- post = self.listdir + 'archive' + hashdir.to_s + message.to_s
- return unless post.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 ]
+
+ match = message[1].match( /^(?<date>[^;]+);(?<author_id>\w+) / )
+ next unless match
+ author_id = match[ :author_id ]
+ date = match[ :date ]
- return Mail.read( post.to_s )
+ metadata = {
+ date: Time.parse( date ),
+ thread: thread_id,
+ author: author_id
+ }
+ acc << metadata
+ end
+ end
+
+ return idx
end
@@ -670,7 +741,7 @@
h = 5381
over = 2 ** ADDRESS_SPACE
- addr = 'T' + addr
+ addr = 'T' + addr.downcase
addr.each_char do |c|
h = ( h + ( h << 5 ) ) ^ c.ord
h = h % over if h > over # emulate integer overflow
@@ -679,7 +750,7 @@
end
- ### Given an email address, return the ascii character.
+ ### Given an email address, return the ascii hash prefix.
###
def hashchar( addr )
return ( self.subhash(addr) + 64 ).chr