equal
deleted
inserted
replaced
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. |