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 ### |
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 |
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? |
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? |
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 |