lib/ezmlm/list.rb
changeset 17 23c7f5c8ee39
parent 16 e135ccae6783
child 20 9d59d30685cb
equal deleted inserted replaced
16:e135ccae6783 17:23c7f5c8ee39
     1 #!/usr/bin/ruby
     1 #!/usr/bin/ruby
     2 # vim: set nosta noet ts=4 sw=4:
     2 # vim: set nosta noet ts=4 sw=4:
       
     3 
       
     4 
       
     5 # A Ruby interface to a single Ezmlm-idx mailing list directory.
     3 #
     6 #
     4 # A Ruby interface to a single Ezmlm-idx mailing list directory.
     7 #    list = Ezmlm::List.new( '/path/to/listdir' )
       
     8 #
     5 #
     9 #
     6 # == Version
    10 # == Version
     7 #
    11 #
     8 #  $Id$
    12 #  $Id$
     9 #
    13 #
    26 	### Create a new Ezmlm::List object for the specified +listdir+, which should be
    30 	### Create a new Ezmlm::List object for the specified +listdir+, which should be
    27 	### an ezmlm-idx mailing list directory.
    31 	### an ezmlm-idx mailing list directory.
    28 	###
    32 	###
    29 	def initialize( listdir )
    33 	def initialize( listdir )
    30 		listdir = Pathname.new( listdir ) unless listdir.is_a?( Pathname )
    34 		listdir = Pathname.new( listdir ) unless listdir.is_a?( Pathname )
       
    35 		unless listdir.directory? && ( listdir + 'mailinglist' ).exist?
       
    36 			raise ArgumentError, "%p doesn't appear to be an ezmlm-idx list." % [ listdir.to_s ]
       
    37 		end
    31 		@listdir = listdir
    38 		@listdir = listdir
    32 	end
    39 	end
    33 
    40 
    34 	# The Pathname object for the list directory
    41 	# The Pathname object for the list directory
    35 	attr_reader :listdir
    42 	attr_reader :listdir
   217 	def remove_allowed( *addr )
   224 	def remove_allowed( *addr )
   218 		return self.unsubscribe( *addr, section: 'allow' )
   225 		return self.unsubscribe( *addr, section: 'allow' )
   219 	end
   226 	end
   220 
   227 
   221 
   228 
   222 	### Returns +true+ if message threading is enabled.
       
   223 	###
       
   224 	def threaded?
       
   225 		return ( self.listdir + 'threaded' ).exist?
       
   226 	end
       
   227 
       
   228 	### Disable or enable message threading.
       
   229 	###
       
   230 	### This automatically builds message indexes and thread
       
   231 	### information on an incoming message.
       
   232 	###
       
   233 	def threaded=( enable=true )
       
   234 		if enable
       
   235 			self.touch( 'threaded' )
       
   236 		else
       
   237 			self.unlink( 'threaded' )
       
   238 		end
       
   239 	end
       
   240 	alias_method :threaded, :threaded=
       
   241 
       
   242 
       
   243 	### Returns +true+ if the list is configured to respond
   229 	### Returns +true+ if the list is configured to respond
   244 	### to remote management requests.
   230 	### to remote management requests.
   245 	###
   231 	###
   246 	def public?
   232 	def public?
   247 		return ( self.listdir + 'public' ).exist?
   233 		return ( self.listdir + 'public' ).exist?
   378 
   364 
   379 
   365 
   380 	### Returns +true+ if message archival is enabled.
   366 	### Returns +true+ if message archival is enabled.
   381 	###
   367 	###
   382 	def archived?
   368 	def archived?
   383 		return ( self.listdir + 'archived' ).exist? || ( self.listdir + 'indexed' ).exist?
   369 		test = %w[ archived indexed threaded ].each_with_object( [] ) do |f, acc|
   384 	end
   370 			acc << self.listdir + f
   385 
   371 		end
   386 	### Disable or enable message archiving (and indexing.)
   372 
   387 	###
   373 		return test.all?( &:exist? )
   388 	def archive=( enable=true )
   374 	end
   389 		if enable
   375 
   390 			self.touch( 'archived' )
   376 	### Disable or enable message archiving (and indexing/threading.)
   391 			self.touch( 'indexed' )
   377 	###
   392 		else
   378 	def archived=( enable=true )
   393 			self.unlink( 'archived' )
   379 		if enable
   394 			self.unlink( 'indexed' )
   380 			self.touch( 'archived', 'indexed', 'threaded' )
   395 		end
   381 		else
   396 	end
   382 			self.unlink( 'archived', 'indexed', 'threaded' )
   397 	alias_method :archive, :archive=
   383 		end
       
   384 	end
       
   385 	alias_method :archived, :archived=
   398 
   386 
   399 	### Returns +true+ if the message archive is accessible only to
   387 	### Returns +true+ if the message archive is accessible only to
   400 	### moderators.
   388 	### moderators.
   401 	###
   389 	###
   402 	def private_archive?
   390 	def private_archive?
   651 	end
   639 	end
   652 
   640 
   653 	### Returns an individual message if archiving was enabled.
   641 	### Returns an individual message if archiving was enabled.
   654 	###
   642 	###
   655 	def message( message_id )
   643 	def message( message_id )
   656 		raise "Archiving is not enabled." unless self.archived?
       
   657 		raise "Message archive is empty." if self.message_count.zero?
   644 		raise "Message archive is empty." if self.message_count.zero?
   658 		return Ezmlm::List::Message.new( self, message_id )
   645 		return Ezmlm::List::Message.new( self, message_id ) rescue nil
   659 	end
   646 	end
   660 
   647 
   661 	### Lazy load each message ID as a Ezmlm::List::Message,
   648 	### Lazy load each message ID as a Ezmlm::List::Message,
   662 	### yielding it to the block.
   649 	### yielding it to the block.
   663 	###
   650 	###
   669 
   656 
   670 
   657 
   671 	### Return a Thread object for the given +thread_id+.
   658 	### Return a Thread object for the given +thread_id+.
   672 	###
   659 	###
   673 	def thread( thread_id )
   660 	def thread( thread_id )
   674 		raise "Archiving is not enabled." unless self.archived?
   661 		return Ezmlm::List::Thread.new( self, thread_id ) rescue nil
   675 		return Ezmlm::List::Thread.new( self, thread_id )
       
   676 	end
   662 	end
   677 
   663 
   678 
   664 
   679 	### Return an Author object for the given +author_id+, which
   665 	### Return an Author object for the given +author_id+, which
   680 	### could also be an email address.
   666 	### could also be an email address.
   681 	###
   667 	###
   682 	def author( author_id )
   668 	def author( author_id )
   683 		raise "Archiving is not enabled." unless self.archived?
       
   684 		author_id = Ezmlm::Hash.address(author_id) if author_id.index( '@' )
   669 		author_id = Ezmlm::Hash.address(author_id) if author_id.index( '@' )
   685 		return Ezmlm::List::Author.new( self, author_id )
   670 		return Ezmlm::List::Author.new( self, author_id ) rescue nil
   686 	end
   671 	end
   687 
   672 
   688 
   673 
   689 	### Parse all thread indexes into a single array that can be used
   674 	### Parse all thread indexes into a single array that can be used
   690 	### as a lookup table.
   675 	### as a lookup table.
   698 
   683 
   699 		idx = ( 0 .. self.message_count / 100 ).each_with_object( [] ) do |dir, acc|
   684 		idx = ( 0 .. self.message_count / 100 ).each_with_object( [] ) do |dir, acc|
   700 			index = archivedir + dir.to_s + 'index'
   685 			index = archivedir + dir.to_s + 'index'
   701 			next unless index.exist?
   686 			next unless index.exist?
   702 
   687 
   703 			index.each_line.lazy.slice_before( /^\d+:/ ).each do |message|
   688 			index.open( 'r', encoding: Encoding::ISO8859_1 ) do |fh|
   704 				match = message[0].match( /^(?<message_id>\d+): (?<thread_id>\w+)/ )
   689 				fh.each_line.lazy.slice_before( /^\d+:/ ).each do |message|
   705 				next unless match
   690 
   706 				thread_id  = match[ :thread_id ]
   691 					match = message[0].match( /^(?<message_id>\d+): (?<thread_id>\w+)/ )
   707 
   692 					next unless match
   708 				match = message[1].match( /^(?<date>[^;]+);(?<author_id>\w+) / )
   693 					thread_id  = match[ :thread_id ]
   709 				next unless match
   694 
   710 				author_id  = match[ :author_id ]
   695 					match = message[1].match( /^(?<date>[^;]+);(?<author_id>\w+) / )
   711 				date       = match[ :date ]
   696 					next unless match
   712 
   697 					author_id  = match[ :author_id ]
   713 				metadata = {
   698 					date       = match[ :date ]
   714 					date:   Time.parse( date ),
   699 
   715 					thread: thread_id,
   700 					metadata = {
   716 					author: author_id
   701 						date:   Time.parse( date ),
   717 				}
   702 						thread: thread_id,
   718 				acc << metadata
   703 						author: author_id
       
   704 					}
       
   705 					acc << metadata
       
   706 				end
   719 			end
   707 			end
   720 		end
   708 		end
   721 
   709 
   722 		return idx
   710 		return idx
   723 	end
   711 	end
   755 	end
   743 	end
   756 
   744 
   757 
   745 
   758 	### Simply create an empty file, safely.
   746 	### Simply create an empty file, safely.
   759 	###
   747 	###
   760 	def touch( file )
   748 	def touch( *file )
   761 		self.write( file ) {}
   749 		self.with_safety do
       
   750 			Array( file ).flatten.each do |f|
       
   751 				f = self.listdir + f unless f.is_a?( Pathname )
       
   752 				f.open( 'w' ) {}
       
   753 			end
       
   754 		end
   762 	end
   755 	end
   763 
   756 
   764 
   757 
   765 	### Delete +file+ safely.
   758 	### Delete +file+ safely.
   766 	###
   759 	###
   767 	def unlink( file )
   760 	def unlink( *file )
   768 		file = self.listdir + file unless file.is_a?( Pathname )
       
   769 		return unless file.exist?
       
   770 		self.with_safety do
   761 		self.with_safety do
   771 			file.unlink
   762 			Array( file ).flatten.each do |f|
       
   763 				f = self.listdir + f unless f.is_a?( Pathname )
       
   764 				next unless f.exist?
       
   765 				f.unlink
       
   766 			end
   772 		end
   767 		end
   773 	end
   768 	end
   774 
   769 
   775 
   770 
   776 	### Return a Pathname to a subscription directory.
   771 	### Return a Pathname to a subscription directory.