lib/ezmlm/list.rb
changeset 15 a38e6916504c
parent 14 cba9fb39bcdb
child 16 e135ccae6783
equal deleted inserted replaced
14:cba9fb39bcdb 15:a38e6916504c
     8 #  $Id$
     8 #  $Id$
     9 #
     9 #
    10 #---
    10 #---
    11 
    11 
    12 require 'pathname'
    12 require 'pathname'
       
    13 require 'time'
    13 require 'etc'
    14 require 'etc'
    14 require 'ezmlm'
    15 require 'ezmlm' unless defined?( Ezmlm )
    15 require 'mail'
       
    16 
    16 
    17 
    17 
    18 ### A Ruby interface to an ezmlm-idx mailing list directory
    18 ### A Ruby interface to an ezmlm-idx mailing list directory
    19 ###
    19 ###
    20 class Ezmlm::List
    20 class Ezmlm::List
    21 
    21 
    22 	# Quick address space detection, to (hopefully)
    22 	# Quick address space detection, to (hopefully)
    23 	# match the overflow size on this machine.
    23 	# match the overflow size on this machine.
    24 	#
    24 	ADDRESS_SPACE = [ 'i' ].pack( 'p' ).size * 8
    25 	ADDRESS_SPACE = case [ 'i' ].pack( 'p' ).size
       
    26 					when 4
       
    27 						32
       
    28 					when 8
       
    29 						64
       
    30 					end
       
    31 
    25 
    32 	# Valid subdirectories/sections for subscriptions.
    26 	# Valid subdirectories/sections for subscriptions.
    33 	SUBSCRIPTION_DIRS = %w[ deny mod digest allow ]
    27 	SUBSCRIPTION_DIRS = %w[ deny mod digest allow ]
    34 
    28 
    35 
    29 
   245 			self.touch( 'threaded' )
   239 			self.touch( 'threaded' )
   246 		else
   240 		else
   247 			self.unlink( 'threaded' )
   241 			self.unlink( 'threaded' )
   248 		end
   242 		end
   249 	end
   243 	end
       
   244 	alias_method :threaded, :threaded=
   250 
   245 
   251 
   246 
   252 	### Returns +true+ if the list is configured to respond
   247 	### Returns +true+ if the list is configured to respond
   253 	### to remote mangement requests.
   248 	### to remote management requests.
   254 	###
   249 	###
   255 	def public?
   250 	def public?
   256 		return ( self.listdir + 'public' ).exist?
   251 		return ( self.listdir + 'public' ).exist?
   257 	end
   252 	end
   258 
   253 
   263 			self.touch( 'public' )
   258 			self.touch( 'public' )
   264 		else
   259 		else
   265 			self.unlink( 'public' )
   260 			self.unlink( 'public' )
   266 		end
   261 		end
   267 	end
   262 	end
       
   263 	alias_method :public, :public=
   268 
   264 
   269 	### Returns +true+ if the list is not configured to respond
   265 	### Returns +true+ if the list is not configured to respond
   270 	### to remote mangement requests.
   266 	### to remote management requests.
   271 	###
   267 	###
   272 	def private?
   268 	def private?
   273 		return ! self.public?
   269 		return ! self.public?
   274 	end
   270 	end
   275 
   271 
   276 	### Disable or enable remote management requests.
   272 	### Disable or enable remote management requests.
   277 	###
   273 	###
   278 	def private=( enable=false )
   274 	def private=( enable=false )
   279 		self.public = ! enable
   275 		self.public = ! enable
   280 	end
   276 	end
       
   277 	alias_method :private, :private=
   281 
   278 
   282 
   279 
   283 	### Returns +true+ if the list supports remote administration
   280 	### Returns +true+ if the list supports remote administration
   284 	### subscribe/unsubscribe requests from moderators.
   281 	### subscribe/unsubscribe requests from moderators.
   285 	###
   282 	###
   294 			self.touch( 'remote' )
   291 			self.touch( 'remote' )
   295 		else
   292 		else
   296 			self.unlink( 'remote' )
   293 			self.unlink( 'remote' )
   297 		end
   294 		end
   298 	end
   295 	end
       
   296 	alias_method :remote_subscriptions, :remote_subscriptions=
   299 
   297 
   300 
   298 
   301 	### Returns +true+ if list subscription requests require moderator
   299 	### Returns +true+ if list subscription requests require moderator
   302 	### approval.
   300 	### approval.
   303 	###
   301 	###
   312 			self.touch( 'modsub' )
   310 			self.touch( 'modsub' )
   313 		else
   311 		else
   314 			self.unlink( 'modsub' )
   312 			self.unlink( 'modsub' )
   315 		end
   313 		end
   316 	end
   314 	end
   317 
   315 	alias_method :moderated_subscriptions, :moderated_subscriptions=
   318 
   316 
   319 	### Returns +true+ if message moderation is enabled.
   317 	### Returns +true+ if message moderation is enabled.
   320 	###
   318 	###
   321 	def moderated?
   319 	def moderated?
   322 		return ( self.listdir + 'modpost' ).exist?
   320 		return ( self.listdir + 'modpost' ).exist?
   323 	end
   321 	end
   324 
   322 
   325 	### Disable or enable message moderation.
   323 	### Disable or enable message moderation.
   326 	###
   324 	###
   327 	### This has special meaning when combined with user_post_only setting.
   325 	### This has special meaning when combined with user_posts_only setting.
   328 	### Lists act as unmoderated for subscribers, and posts from unknown
   326 	### Lists act as unmoderated for subscribers, and posts from unknown
   329 	### addresses go to moderation.
   327 	### addresses go to moderation.
   330 	###
   328 	###
   331 	def moderated=( enable=false )
   329 	def moderated=( enable=false )
   332 		if enable
   330 		if enable
   335 		else
   333 		else
   336 			self.unlink( 'modpost' )
   334 			self.unlink( 'modpost' )
   337 			self.unlink( 'noreturnposts' ) if self.user_posts_only?
   335 			self.unlink( 'noreturnposts' ) if self.user_posts_only?
   338 		end
   336 		end
   339 	end
   337 	end
       
   338 	alias_method :moderated, :moderated=
   340 
   339 
   341 
   340 
   342 	### Returns +true+ if posting is only allowed by moderators.
   341 	### Returns +true+ if posting is only allowed by moderators.
   343 	###
   342 	###
   344 	def moderator_posts_only?
   343 	def moderator_posts_only?
   352 			self.touch( 'modpostonly' )
   351 			self.touch( 'modpostonly' )
   353 		else
   352 		else
   354 			self.unlink( 'modpostonly' )
   353 			self.unlink( 'modpostonly' )
   355 		end
   354 		end
   356 	end
   355 	end
       
   356 	alias_method :moderator_posts_only, :moderator_posts_only=
   357 
   357 
   358 
   358 
   359 	### Returns +true+ if posting is only allowed by subscribers.
   359 	### Returns +true+ if posting is only allowed by subscribers.
   360 	###
   360 	###
   361 	def user_posts_only?
   361 	def user_posts_only?
   376 		else
   376 		else
   377 			self.unlink( 'subpostonly' )
   377 			self.unlink( 'subpostonly' )
   378 			self.unlink( 'noreturnposts' ) if self.moderated?
   378 			self.unlink( 'noreturnposts' ) if self.moderated?
   379 		end
   379 		end
   380 	end
   380 	end
   381 
   381 	alias_method :user_posts_only, :user_posts_only=
   382 
   382 
   383 
   383 
   384 	### Returns +true+ if message archival is enabled.
   384 	### Returns +true+ if message archival is enabled.
   385 	###
   385 	###
   386 	def archived?
   386 	def archived?
   396 		else
   396 		else
   397 			self.unlink( 'archived' )
   397 			self.unlink( 'archived' )
   398 			self.unlink( 'indexed' )
   398 			self.unlink( 'indexed' )
   399 		end
   399 		end
   400 	end
   400 	end
       
   401 	alias_method :archive, :archive=
   401 
   402 
   402 	### Returns +true+ if the message archive is accessible only to
   403 	### Returns +true+ if the message archive is accessible only to
   403 	### moderators.
   404 	### moderators.
   404 	###
   405 	###
   405 	def private_archive?
   406 	def private_archive?
   413 			self.touch( 'modgetonly' )
   414 			self.touch( 'modgetonly' )
   414 		else
   415 		else
   415 			self.unlink( 'modgetonly' )
   416 			self.unlink( 'modgetonly' )
   416 		end
   417 		end
   417 	end
   418 	end
       
   419 	alias_method :private_archive, :private_archive=
   418 
   420 
   419 	### Returns +true+ if the message archive is accessible to anyone.
   421 	### Returns +true+ if the message archive is accessible to anyone.
   420 	###
   422 	###
   421 	def public_archive?
   423 	def public_archive?
   422 		return ! self.private_archive?
   424 		return ! self.private_archive?
   423 	end
   425 	end
   424 
   426 
       
   427 	### Disable or enable private access to the archive.
       
   428 	###
       
   429 	def public_archive=( enable=true )
       
   430 		self.private_archive = ! enable
       
   431 	end
       
   432 	alias_method :public_archive, :public_archive=
       
   433 
   425 	### Returns +true+ if the message archive is accessible only to
   434 	### Returns +true+ if the message archive is accessible only to
   426 	### list subscribers.
   435 	### list subscribers.
   427 	###
   436 	###
   428 	def guarded_archive?
   437 	def guarded_archive?
   429 		return ( self.listdir + 'subgetonly' ).exist?
   438 		return ( self.listdir + 'subgetonly' ).exist?
   436 			self.touch( 'subgetonly' )
   445 			self.touch( 'subgetonly' )
   437 		else
   446 		else
   438 			self.unlink( 'subgetonly' )
   447 			self.unlink( 'subgetonly' )
   439 		end
   448 		end
   440 	end
   449 	end
       
   450 	alias_method :guarded_archive, :guarded_archive=
   441 
   451 
   442 
   452 
   443 	### Returns +true+ if message digests are enabled.
   453 	### Returns +true+ if message digests are enabled.
   444 	###
   454 	###
   445 	def digested?
   455 	def digested?
   453 			self.touch( 'digested' )
   463 			self.touch( 'digested' )
   454 		else
   464 		else
   455 			self.unlink( 'digested' )
   465 			self.unlink( 'digested' )
   456 		end
   466 		end
   457 	end
   467 	end
       
   468 	alias_method :digest, :digest=
   458 
   469 
   459 	### If the list is digestable, trigger the digest after this amount
   470 	### If the list is digestable, trigger the digest after this amount
   460 	### of message body since the latest digest, in kbytes.
   471 	### of message body since the latest digest, in kbytes.
   461 	###
   472 	###
   462 	### See: ezmlm-tstdig(1)
   473 	### See: ezmlm-tstdig(1)
   529 			self.unlink( 'nosubconfirm' )
   540 			self.unlink( 'nosubconfirm' )
   530 		else
   541 		else
   531 			self.touch( 'nosubconfirm' )
   542 			self.touch( 'nosubconfirm' )
   532 		end
   543 		end
   533 	end
   544 	end
       
   545 	alias_method :confirm_subscriptions, :confirm_subscriptions=
   534 
   546 
   535 	### Returns +true+ if the list requires unsubscriptions to be
   547 	### Returns +true+ if the list requires unsubscriptions to be
   536 	### confirmed.  AKA "jump" mode.
   548 	### confirmed.  AKA "jump" mode.
   537 	###
   549 	###
   538 	def confirm_unsubscriptions?
   550 	def confirm_unsubscriptions?
   547 			self.unlink( 'nounsubconfirm' )
   559 			self.unlink( 'nounsubconfirm' )
   548 		else
   560 		else
   549 			self.touch( 'nounsubconfirm' )
   561 			self.touch( 'nounsubconfirm' )
   550 		end
   562 		end
   551 	end
   563 	end
       
   564 	alias_method :confirm_unsubscriptions, :confirm_unsubscriptions=
   552 
   565 
   553 
   566 
   554 	### Returns +true+ if the list requires regular message postings
   567 	### Returns +true+ if the list requires regular message postings
   555 	### to be confirmed by the original sender.
   568 	### to be confirmed by the original sender.
   556 	###
   569 	###
   565 			self.touch( 'confirmpost' )
   578 			self.touch( 'confirmpost' )
   566 		else
   579 		else
   567 			self.unlink( 'confirmpost' )
   580 			self.unlink( 'confirmpost' )
   568 		end
   581 		end
   569 	end
   582 	end
       
   583 	alias_method :confirm_postings, :confirm_postings=
   570 
   584 
   571 
   585 
   572 	### Returns +true+ if the list allows moderators to
   586 	### Returns +true+ if the list allows moderators to
   573 	### fetch a subscriber list remotely.
   587 	### fetch a subscriber list remotely.
   574 	###
   588 	###
   584 			self.touch( 'modcanlist' )
   598 			self.touch( 'modcanlist' )
   585 		else
   599 		else
   586 			self.unlink( 'modcanlist' )
   600 			self.unlink( 'modcanlist' )
   587 		end
   601 		end
   588 	end
   602 	end
       
   603 	alias_method :allow_remote_listing, :allow_remote_listing=
   589 
   604 
   590 
   605 
   591 	### Returns +true+ if the list automatically manages
   606 	### Returns +true+ if the list automatically manages
   592 	### bouncing subscriber addresses.
   607 	### bouncing subscriber addresses.
   593 	###
   608 	###
   602 			self.unlink( 'nowarn' )
   617 			self.unlink( 'nowarn' )
   603 		else
   618 		else
   604 			self.touch( 'nowarn' )
   619 			self.touch( 'nowarn' )
   605 		end
   620 		end
   606 	end
   621 	end
       
   622 	alias_method :bounce_warnings, :bounce_warnings=
   607 
   623 
   608 
   624 
   609 	### Return the maximum message size, in bytes.  Messages larger than
   625 	### Return the maximum message size, in bytes.  Messages larger than
   610 	### this size will be rejected.
   626 	### this size will be rejected.
   611 	###
   627 	###
   615 		size = self.read( 'msgsize' )
   631 		size = self.read( 'msgsize' )
   616 		return size ? size.split( ':' ).first.to_i : 0
   632 		return size ? size.split( ':' ).first.to_i : 0
   617 	end
   633 	end
   618 
   634 
   619 	### Set the maximum message size, in bytes.  Messages larger than
   635 	### Set the maximum message size, in bytes.  Messages larger than
   620 	### this size will be rejected.
   636 	### this size will be rejected.  Defaults to 300kb.
   621 	###
   637 	###
   622 	### See: ezmlm-reject(1)
   638 	### See: ezmlm-reject(1)
   623 	###
   639 	###
   624 	def maximum_message_size=( size=307200 )
   640 	def maximum_message_size=( size=307200 )
   625 		if size.to_i.zero?
   641 		if size.to_i.zero?
   628 			self.write( 'msgsize' ) {|f| f.puts "#{size.to_i}:0" }
   644 			self.write( 'msgsize' ) {|f| f.puts "#{size.to_i}:0" }
   629 		end
   645 		end
   630 	end
   646 	end
   631 
   647 
   632 
   648 
       
   649 
   633 	### Return the number of messages in the list archive.
   650 	### Return the number of messages in the list archive.
   634 	###
   651 	###
   635 	def message_count
   652 	def message_count
   636 		count = self.read( 'archnum' )
   653 		count = self.read( 'archnum' )
   637 		return count ? Integer( count ) : 0
   654 		return count ? Integer( count ) : 0
   638 	end
   655 	end
   639 
   656 
   640 	### Returns the last message to the list as a Mail::Message, if
   657 	### Returns an individual message if archiving was enabled.
   641 	### archiving was enabled.
   658 	###
   642 	###
   659 	def message( message_id )
   643 	def last_post
   660 		raise "Archiving is not enabled." unless self.archived?
   644 		num = self.message_count
   661 		raise "Message archive is empty." if self.message_count.zero?
   645 		return if num.zero?
   662 		return Ezmlm::List::Message.new( self, message_id )
   646 
   663 	end
   647 		hashdir = num / 100
   664 
   648 		message = "%02d" % [ num % 100 ]
   665 	### Lazy load each message ID as a Ezmlm::List::Message,
   649 
   666 	### yielding it to the block.
   650 		post = self.listdir + 'archive' + hashdir.to_s + message.to_s
   667 	###
   651 		return unless post.exist?
   668 	def each_message
   652 
   669 		( 1 .. self.message_count ).each do |id|
   653 		return Mail.read( post.to_s )
   670 			yield self.message( id )
       
   671 		end
       
   672 	end
       
   673 
       
   674 
       
   675 	### Return a Thread object for the given +thread_id+.
       
   676 	###
       
   677 	def thread( thread_id )
       
   678 		raise "Archiving is not enabled." unless self.archived?
       
   679 		return Ezmlm::List::Thread.new( self, thread_id )
       
   680 	end
       
   681 
       
   682 
       
   683 	### Return an Author object for the given +author_id+.
       
   684 	###
       
   685 	def author( author_id )
       
   686 		raise "Archiving is not enabled." unless self.archived?
       
   687 		return Ezmlm::List::Author.new( self, author_id )
       
   688 	end
       
   689 
       
   690 
       
   691 	### Parse all thread indexes into a single array that can be used
       
   692 	### as a lookup table.
       
   693 	###
       
   694 	### These are not expanded into objects, use #message, #thread,
       
   695 	### and #author to do so.
       
   696 	###
       
   697 	def index
       
   698 		raise "Archiving is not enabled." unless self.archived?
       
   699 		archivedir = listdir + 'archive'
       
   700 
       
   701 		idx = ( 0 .. self.message_count / 100 ).each_with_object( [] ) do |dir, acc|
       
   702 			index = archivedir + dir.to_s + 'index'
       
   703 			next unless index.exist?
       
   704 
       
   705 			index.each_line.lazy.slice_before( /^\d+:/ ).each do |message|
       
   706 				match = message[0].match( /^(?<message_id>\d+): (?<thread_id>\w+)/ )
       
   707 				next unless match
       
   708 				thread_id  = match[ :thread_id ]
       
   709 
       
   710 				match = message[1].match( /^(?<date>[^;]+);(?<author_id>\w+) / )
       
   711 				next unless match
       
   712 				author_id  = match[ :author_id ]
       
   713 				date       = match[ :date ]
       
   714 
       
   715 				metadata = {
       
   716 					date:   Time.parse( date ),
       
   717 					thread: thread_id,
       
   718 					author: author_id
       
   719 				}
       
   720 				acc << metadata
       
   721 			end
       
   722 		end
       
   723 
       
   724 		return idx
   654 	end
   725 	end
   655 
   726 
   656 
   727 
   657 	#########
   728 	#########
   658 	protected
   729 	protected
   668 	###
   739 	###
   669 	def subhash( addr )
   740 	def subhash( addr )
   670 		h = 5381
   741 		h = 5381
   671 		over = 2 ** ADDRESS_SPACE
   742 		over = 2 ** ADDRESS_SPACE
   672 
   743 
   673 		addr = 'T' + addr
   744 		addr = 'T' + addr.downcase
   674 		addr.each_char do |c|
   745 		addr.each_char do |c|
   675 			h = ( h + ( h << 5 ) ) ^ c.ord
   746 			h = ( h + ( h << 5 ) ) ^ c.ord
   676 			h = h % over if h > over # emulate integer overflow
   747 			h = h % over if h > over # emulate integer overflow
   677 		end
   748 		end
   678 		return h % 53
   749 		return h % 53
   679 	end
   750 	end
   680 
   751 
   681 
   752 
   682 	### Given an email address, return the ascii character.
   753 	### Given an email address, return the ascii hash prefix.
   683 	###
   754 	###
   684 	def hashchar( addr )
   755 	def hashchar( addr )
   685 		return ( self.subhash(addr) + 64 ).chr
   756 		return ( self.subhash(addr) + 64 ).chr
   686 	end
   757 	end
   687 
   758